Learn / Programming / TypeScript / TypeScript in Real Projects

Intermediate 15 min

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 chaining

Avoid 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 allowJs and rename files from .js to .ts one at a time.
  • Start with the shared models and utility functions, then the leaves.
  • Use // @ts-expect-error with a comment for known issues instead of any everywhere.
  • Run tsc --noEmit in CI so type errors fail the build.
Declaration files

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());
}