TypeScript in Real Projects
tsconfig, strict mode, typing APIs and React props.
What you will learn
- Configure strict mode
- Type API responses safely
- Type React components
Moving from exercises to a real project involves configuration, typing data that comes from outside your code, and typing your framework. This lesson covers the practical parts.
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src"]
}"strict": true turns on the checks that make TypeScript worth using: no implicit any, and strict null checks (a value that may be null must be handled). Start every new project with it enabled.
function firstWord(s: string | null): string {
if (s === null) return "";
return s.split(" ")[0];
}
const el = document.querySelector("#name"); // Element | null
el?.classList.add("active"); // optional chainingAvoid the non-null assertion el! unless you are truly certain; it just silences the compiler.
Typing data from an API
response.json() returns any, and types are erased at runtime, so an annotation alone does not prove the data matches. Validate at the boundary with a schema library like zod and derive the type from it.
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
async function getUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return UserSchema.parse(await res.json()); // throws if the shape is wrong
}import { useState } from "react";
interface ButtonProps {
label: string;
variant?: "primary" | "danger";
onClick: () => void;
}
function Button({ label, variant = "primary", onClick }: ButtonProps) {
return <button className={variant} onClick={onClick}>{label}</button>;
}
function Counter() {
const [items, setItems] = useState<string[]>([]); // give state an explicit type
const [user, setUser] = useState<User | null>(null);
return <Button label="Add" onClick={() => setItems([...items, "x"])} />;
}Migrating gradually
- Enable
allowJsand rename files from.jsto.tsone at a time. - Start with the shared models and utility functions, then the leaves.
- Use
// @ts-expect-errorwith a comment for known issues instead ofanyeverywhere. - Run
tsc --noEmitin CI so type errors fail the build.
Libraries without built-in types have community ones: npm install --save-dev @types/node gives you types for Node.js.
Try it yourself
Write a zod schema for a Todo (id, title, done), derive its type and write a typed fetchTodos() returning Todo[].
Show solution
const TodoSchema = z.object({ id: z.number(), title: z.string(), done: z.boolean() });
type Todo = z.infer<typeof TodoSchema>;
async function fetchTodos(): Promise<Todo[]> {
const res = await fetch("/api/todos");
return z.array(TodoSchema).parse(await res.json());
}