Learn / Programming / TypeScript / TypeScript in Real Projects

TypeScript · Lesson 4 of 4

TypeScript in Real Projects

tsconfig, strict mode, typing APIs and React props.

  • Intermediate
  • 15 min read
  • 3 objectives

Before this lessonLesson 3: Generics and Utility Types

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.

tsconfig and strict mode

{
  "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.

Null safety

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
}

Typing React

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.
// Write your solution here
Course completeYou finished TypeScriptReview the full course or pick your next one.