| name | mastering-typescript |
| description | Write, review, debug, and improve TypeScript code across any context: React SPAs, Node.js tooling, CLI scripts, automation, monorepos, and API backends. Use this skill whenever the user asks about TypeScript types, generics, tsconfig, type errors, Zod validation, type narrowing, mapped/conditional types, path aliases, project references, JS-to-TS migration, or typed React components and hooks. Also trigger when the user pastes a TypeScript error they don't understand, or asks how to type something specific
|
Mastering TypeScript
TypeScript 5.x · Node.js 22 LTS · React 19 · strict mode first
Step 0: Understand the context first
Before writing or reviewing TypeScript, identify:
- Environment — browser SPA, Node.js CLI/tooling, monorepo, CI script?
- Entry point — Vite, tsc, ts-node, tsx, esbuild?
- Strict mode? — If not, enable it. Explain why if they push back.
- Framework — React, NestJS, plain Node, none?
- Existing tsconfig? — Ask or check before proposing settings.
If the user pastes a tsc error, diagnose it first — skip the interview.
The tsconfig Foundation
Strict baseline (works everywhere)
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
What strict: true enables: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, useUnknownInCatchVariables.
Monorepo / path aliases
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@shared/*": ["src/shared/*"],
"@features/*": ["src/features/*"]
}
}
}
⚠️ Path aliases in tsconfig are type-only. Runtime resolution requires Vite's resolve.alias, webpack's alias, or tsconfig-paths for Node.
Project references (monorepo)
{
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/sdk" },
{ "path": "./packages/e2e" }
]
}
{
"compilerOptions": {
"composite": true,
"declarationMap": true
},
"references": [{ "path": "../shared" }]
}
Use tsc --build instead of tsc in monorepos.
Type System Quick Reference
Primitives and literals
const name: string = "Alice";
const port: number = 3000;
const active: boolean = true;
type Status = "pending" | "approved" | "rejected";
type Direction = "north" | "south" | "east" | "west";
Union and intersection
type StringOrNumber = string | number;
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function handle<T>(r: Result<T>): T | null {
if (r.success) return r.data;
console.error(r.error);
return null;
}
type Admin = User & { permissions: string[] };
The satisfies operator (TS 4.9+)
const config = { port: 3000, host: "localhost" } as Record<string, unknown>;
config.port.toFixed(2);
const config = {
port: 3000,
host: "localhost"
} satisfies Record<string, string | number>;
config.port.toFixed(2);
config.host.toUpperCase();
Type guards
function format(val: string | number): string {
if (typeof val === "string") return val.toUpperCase();
return val.toFixed(2);
}
function handleError(e: unknown): string {
if (e instanceof Error) return e.message;
return String(e);
}
interface User { kind: "user"; name: string }
interface Admin { kind: "admin"; permissions: string[] }
function isAdmin(p: User | Admin): p is Admin {
return p.kind === "admin";
}
function assertDefined<T>(val: T | null | undefined, msg: string): asserts val is T {
if (val == null) throw new Error(msg);
}
unknown vs any
function parse(data: any) {
data.whatever.you.want();
}
function parse(data: unknown) {
if (typeof data === "object" && data !== null && "name" in data) {
console.log((data as { name: string }).name);
}
}
Generics
Basic patterns
function first<T>(items: T[]): T | undefined {
return items[0];
}
function getLength<T extends { length: number }>(item: T): number {
return item.length;
}
function zip<A, B>(as: A[], bs: B[]): [A, B][] {
return as.map((a, i) => [a, bs[i]]);
}
type ApiResponse<T = unknown> = {
data: T;
status: number;
timestamp: Date;
};
Utility types reference
| Type | Purpose | Example |
|---|
Partial<T> | All props optional | Partial<User> |
Required<T> | All props required | Required<Config> |
Readonly<T> | Immutable | Readonly<State> |
Pick<T, K> | Select keys | Pick<User, "id" | "name"> |
Omit<T, K> | Remove keys | Omit<User, "passwordHash"> |
Record<K, V> | Typed object | Record<string, number> |
ReturnType<F> | Function return type | ReturnType<typeof fn> |
Parameters<F> | Function params tuple | Parameters<typeof fn> |
Awaited<T> | Unwrap Promise | Awaited<Promise<User>> |
NonNullable<T> | Remove null/undefined | NonNullable<string | null> |
Advanced Types
Conditional types
type IsArray<T> = T extends unknown[] ? true : false;
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
type ArrayElement<T> = T extends (infer E)[] ? E : never;
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
Mapped types
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never
}[keyof T];
Template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">;
type CSSProperty = `${string}-${string}`;
type Endpoint = `/api/${string}`;
Branded types (validation at the type level)
declare const _brand: unique symbol;
type Brand<T, B> = T & { readonly [_brand]: B };
type UserId = Brand<string, "UserId">;
type Email = Brand<string, "Email">;
function createUserId(id: string): UserId {
if (!id.match(/^user_/)) throw new Error("Invalid user ID");
return id as UserId;
}
function getUser(id: UserId): Promise<User> { ... }
getUser("user_123");
getUser(createUserId("user_123"));
Error Handling
Result pattern (no exceptions)
type Ok<T> = { success: true; data: T };
type Err<E> = { success: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;
const ok = <T>(data: T): Ok<T> => ({ success: true, data });
const err = <E>(error: E): Err<E> => ({ success: false, error });
async function fetchUser(id: string): Promise<Result<User>> {
try {
const user = await db.users.findById(id);
if (!user) return err(new Error(`User ${id} not found`));
return ok(user);
} catch (e) {
return err(e instanceof Error ? e : new Error(String(e)));
}
}
const result = await fetchUser("123");
if (result.success) {
console.log(result.data.name);
} else {
console.error(result.error.message);
}
Typed error classes
abstract class AppError extends Error {
abstract readonly code: string;
abstract readonly statusCode: number;
constructor(message: string) {
super(message);
this.name = this.constructor.name;
}
}
class NotFoundError extends AppError {
readonly code = "NOT_FOUND";
readonly statusCode = 404;
constructor(resource: string, id: string) {
super(`${resource} ${id} not found`);
}
}
class ValidationError extends AppError {
readonly code = "VALIDATION_ERROR";
readonly statusCode = 400;
constructor(message: string, public readonly fields: Record<string, string[]>) {
super(message);
}
}
function isAppError(e: unknown): e is AppError {
return e instanceof AppError;
}
Validation with Zod
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(["user", "admin"]),
});
type User = z.infer<typeof UserSchema>;
const result = UserSchema.safeParse(rawData);
if (result.success) {
console.log(result.data.email);
} else {
console.error(result.error.issues);
}
const CreateUserSchema = UserSchema.omit({ id: true });
const UpdateUserSchema = UserSchema.partial().omit({ id: true });
type CreateUserDto = z.infer<typeof CreateUserSchema>;
type UpdateUserDto = z.infer<typeof UpdateUserSchema>;
const EnvSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
});
export const env = EnvSchema.parse(process.env);
Debugging tsc Errors
Common errors and fixes
TS2322 — Type not assignable
function greet(name: string | undefined) {
const upper = name.toUpperCase();
if (name !== undefined) {
const upper = name.toUpperCase();
}
const upper = (name ?? "stranger").toUpperCase();
const upper = name!.toUpperCase();
}
TS2345 — Argument type mismatch
function process(val: string | number) {
takesString(val);
if (typeof val === "string") takesString(val);
}
TS2339 — Property does not exist
const obj = {};
obj.foo = "bar";
const obj: { foo?: string } = {};
obj.foo = "bar";
TS7006 — Parameter implicitly has 'any' type
const nums = [1, 2, 3];
nums.map(n => n * 2);
nums.forEach(n => { });
function transform(fn: (x: number) => number) { ... }
TS2769 — No overload matches
Usually means you called a generic function with mismatched types.
Check each argument type individually against the function signature.
TS2532 — Object is possibly undefined (with noUncheckedIndexedAccess)
const arr = [1, 2, 3];
const first = arr[0];
const val = first.toFixed(2);
const val = arr[0]?.toFixed(2);
if (first !== undefined) { ... }
Common Mistakes
| Mistake | Problem | Fix |
|---|
any everywhere | No type safety | Use unknown + narrow |
as SomeType without guard | Hides real errors | Use type guards or satisfies |
enum for string unions | Generates runtime JS | Use "a" | "b" | "c" literals |
! non-null assertion | Runtime crash if wrong | Narrow with if instead |
| Not validating API/JSON data | Runtime type mismatch | Use Zod at boundaries |
interface for everything | Can't use mapped types | Use type for aliases & mapped types |
Object.keys() returns string[] | Loses key types | Use (Object.keys(obj) as (keyof typeof obj)[]) |
Reference Files
Load on demand — don't preload all:
references/react.md — Typed components, hooks (useState/useReducer/useRef), events, Context, Zustand, Redux Toolkit
references/toolchain.md — ESLint 9 flat config, Vitest setup, Prettier, pnpm workspaces
references/enterprise.md — Project structure, barrel exports, migration JS→TS, security patterns, branded types