TypeScript Development
Modern TypeScript patterns and type-safe development.
Type Fundamentals
Basic Types
// Primitives const name: string = 'John'; const age: number = 30; const isActive: boolean = true; const nothing: null = null; const notDefined: undefined = undefined;
// Arrays const numbers: number[] = [1, 2, 3]; const strings: Array<string> = ['a', 'b', 'c']; const mixed: (string | number)[] = [1, 'two', 3];
// Tuples const tuple: [string, number] = ['hello', 42]; const namedTuple: [name: string, age: number] = ['John', 30];
// Objects const user: { name: string; age: number } = { name: 'John', age: 30 };
// Any vs Unknown const dangerous: any = getData(); // Avoid - no type checking const safe: unknown = getData(); // Prefer - requires type narrowing if (typeof safe === 'string') { console.log(safe.toUpperCase()); // Now TypeScript knows it's string }
Interfaces vs Types
// Interface - extendable, for objects interface User { id: number; name: string; email: string; }
interface AdminUser extends User { role: 'admin'; permissions: string[]; }
// Type - more flexible type ID = string | number; type Callback = (data: string) => void; type Status = 'pending' | 'active' | 'inactive';
// Intersection types type UserWithTimestamps = User & { createdAt: Date; updatedAt: Date; };
// Use interface for objects, type for unions/primitives
Optional & Readonly
interface Config { required: string; optional?: string; // May be undefined readonly immutable: string; // Can't be reassigned }
// Readonly utility type ReadonlyUser = Readonly<User>;
// Partial - all optional type PartialUser = Partial<User>;
// Required - all required type RequiredUser = Required<User>;
Generics
Basic Generics
// Generic function function identity<T>(value: T): T { return value; }
const num = identity(42); // T inferred as number const str = identity('hello'); // T inferred as string
// Generic interface interface Response<T> { data: T; status: number; message: string; }
const userResponse: Response<User> = { data: { id: 1, name: 'John', email: 'john@example.com' }, status: 200, message: 'Success', };
// Generic class class Queue<T> { private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
}
const numberQueue = new Queue<number>(); numberQueue.enqueue(1);
Constraints
// Constrain to specific shape interface HasId { id: number; }
function findById<T extends HasId>(items: T[], id: number): T | undefined { return items.find(item => item.id === id); }
// Multiple constraints function merge<T extends object, U extends object>(a: T, b: U): T & U { return { ...a, ...b }; }
// keyof constraint function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }
const user = { name: 'John', age: 30 }; const name = getProperty(user, 'name'); // string const age = getProperty(user, 'age'); // number
Utility Types
// Pick - select specific properties type UserPreview = Pick<User, 'id' | 'name'>;
// Omit - exclude properties type UserWithoutEmail = Omit<User, 'email'>;
// Record - map keys to values type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// Extract / Exclude type Status = 'pending' | 'active' | 'deleted'; type ActiveStatus = Extract<Status, 'pending' | 'active'>; // 'pending' | 'active' type WithoutDeleted = Exclude<Status, 'deleted'>; // 'pending' | 'active'
// ReturnType / Parameters function createUser(name: string, email: string): User { return { id: 1, name, email }; }
type CreateUserReturn = ReturnType<typeof createUser>; // User type CreateUserParams = Parameters<typeof createUser>; // [string, string]
// NonNullable type MaybeString = string | null | undefined; type DefinitelyString = NonNullable<MaybeString>; // string
Type Guards
// typeof guard function process(value: string | number) { if (typeof value === 'string') { return value.toUpperCase(); } return value * 2; }
// instanceof guard class Dog { bark() { console.log('Woof!'); } }
class Cat { meow() { console.log('Meow!'); } }
function speak(animal: Dog | Cat) { if (animal instanceof Dog) { animal.bark(); } else { animal.meow(); } }
// in guard interface Bird { fly(): void; } interface Fish { swim(): void; }
function move(animal: Bird | Fish) { if ('fly' in animal) { animal.fly(); } else { animal.swim(); } }
// Custom type guard interface ApiError { code: string; message: string; }
function isApiError(error: unknown): error is ApiError { return ( typeof error === 'object' && error !== null && 'code' in error && 'message' in error ); }
// Usage try { await fetchData(); } catch (error) { if (isApiError(error)) { console.log(error.code); // TypeScript knows it's ApiError } }
Advanced Patterns
Discriminated Unions
interface LoadingState { status: 'loading'; }
interface SuccessState<T> { status: 'success'; data: T; }
interface ErrorState { status: 'error'; error: string; }
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
function handleState<T>(state: AsyncState<T>) { switch (state.status) { case 'loading': return 'Loading...'; case 'success': return state.data; // TypeScript knows data exists case 'error': return state.error; // TypeScript knows error exists } }
Template Literal Types
type EventName = 'click' | 'focus' | 'blur';
type EventHandler = on${Capitalize<EventName>}; // 'onClick' | 'onFocus' | 'onBlur'
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = /api/${string};
function request(method: HTTPMethod, url: Endpoint) { // ... }
request('GET', '/api/users'); // OK request('GET', '/users'); // Error: doesn't start with /api/
Mapped Types
// Make all properties optional type Optional<T> = { [K in keyof T]?: T[K]; };
// Make all properties nullable type Nullable<T> = { [K in keyof T]: T[K] | null; };
// Prefix keys
type Prefixed<T, P extends string> = {
[K in keyof T as ${P}${string & K}]: T[K];
};
type PrefixedUser = Prefixed<User, 'user_'>; // { user_id: number; user_name: string; user_email: string }
Conditional Types
// Basic conditional type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true type B = IsString<number>; // false
// Extract array element type type ElementOf<T> = T extends (infer E)[] ? E : never;
type StringElement = ElementOf<string[]>; // string
// Function return type type AsyncReturnType<T> = T extends (...args: any[]) => Promise<infer R> ? R : never;
async function fetchUser(): Promise<User> { return { id: 1, name: 'John', email: 'john@example.com' }; }
type FetchedUser = AsyncReturnType<typeof fetchUser>; // User
React TypeScript
Component Types
import { FC, ReactNode, ComponentProps } from 'react';
// Props interface interface ButtonProps { variant: 'primary' | 'secondary'; size?: 'sm' | 'md' | 'lg'; children: ReactNode; onClick?: () => void; }
// Function component
function Button({ variant, size = 'md', children, onClick }: ButtonProps) {
return (
<button className={btn-${variant} btn-${size}} onClick={onClick}>
{children}
</button>
);
}
// With FC (includes children) const Card: FC<{ title: string; children: ReactNode }> = ({ title, children }) => ( <div className="card"> <h2>{title}</h2> {children} </div> );
// Extending HTML element props interface InputProps extends ComponentProps<'input'> { label: string; error?: string; }
function Input({ label, error, ...props }: InputProps) { return ( <div> <label>{label}</label> <input {...props} /> {error && <span className="error">{error}</span>} </div> ); }
Hooks
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
// useState with type const [user, setUser] = useState<User | null>(null); const [items, setItems] = useState<string[]>([]);
// useRef const inputRef = useRef<HTMLInputElement>(null); const countRef = useRef<number>(0);
// useCallback with types const handleClick = useCallback((id: number) => { console.log(id); }, []);
// useMemo const expensiveValue = useMemo(() => { return items.filter(item => item.length > 5); }, [items]);
// Custom hook function useLocalStorage<T>(key: string, initialValue: T) { const [value, setValue] = useState<T>(() => { const stored = localStorage.getItem(key); return stored ? JSON.parse(stored) : initialValue; });
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue] as const;
}
Configuration
tsconfig.json
{ "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", "lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
},
"outDir": "dist",
"rootDir": "src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Strict Mode Benefits
// strictNullChecks - catches null/undefined errors const user: User | null = getUser(); user.name; // Error: user might be null user?.name; // OK: optional chaining
// noImplicitAny - requires explicit types function process(data) { } // Error: implicit 'any' function process(data: unknown) { } // OK
// strictPropertyInitialization - ensures class properties are initialized class User { name: string; // Error: not initialized name: string = ''; // OK }