
Typescript Patterns
- 109 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
typescript-patterns is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- typescript-patterns
- AI & Agent Building
- AI-coding skill
Typescript Patterns by the numbers
- 109 all-time installs (skills.sh)
- +9 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,092 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill typescript-patternsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 109 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
TypeScript Patterns
Overview
Advanced TypeScript patterns that use the type system to prevent runtime errors. Focuses on strict mode TypeScript with patterns for type inference, narrowing, and compile-time validation. Not a beginner tutorial.
When to use: Building type-safe APIs, complex data transformations, library authoring, preventing runtime errors through types, working with strict mode flags.
When NOT to use: Learning TypeScript basics (primitives, interfaces, classes), JavaScript-to-TypeScript migration guidance, tooling setup, or build configuration.
Quick Reference
| Pattern | API | Key Points |
|---|---|---|
| Utility types | Pick<T, K>, Omit<T, K> | Extract or exclude properties |
| Partial/Required | Partial<T>, Required<T> | Make properties optional or required |
| Record type | Record<K, V> | Object with known keys |
| Awaited type | Awaited<T> | Unwrap Promise return types |
| ReturnType/Parameters | ReturnType<F>, Parameters<F> | Extract function types |
| Generic constraints | <T extends Type> | Constrain generic parameters |
| Type inference | type Inferred = typeof value | Let TypeScript infer from values |
| Type guards | typeof, instanceof, in | Narrow types at runtime |
| Custom type guards | (x): x is Type | User-defined narrowing functions |
| Inferred predicates | (x) => x !== null | Auto-inferred type predicate filters |
| Discriminated unions | Union with literal type property | Exhaustive pattern matching |
| Conditional types | T extends U ? X : Y | Type-level conditionals |
| Mapped types | { [K in keyof T]: T[K] } | Transform all properties |
| Template literals | ` ${A}-${B} ` | String literal type manipulation |
| Const assertions | as const | Narrowest possible literal types |
| Satisfies operator | value satisfies Type | Type check without widening |
| NoInfer utility | NoInfer<T> | Prevent inference from a position |
| Const type params | <const T extends Type> | Narrow inference without as const |
| Inline type imports | import { type User } | Import types explicitly |
| Module augmentation | declare module 'lib' { ... } | Extend third-party types |
| Assertion functions | function assert(x): asserts x is T | Throw if type guard fails |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
Using any without justification | Use unknown and narrow with type guards |
| Manual type assertions everywhere | Let inference work, type function returns |
| Destructuring before type narrowing | Keep object intact for discriminated unions |
| Index access without undefined check | Enable noUncheckedIndexedAccess |
as Type casting unsafe values | Use satisfies Type to preserve narrow types |
| Inline objects in generics | Extract to const or type alias |
Omitting extends in constraints | Always constrain generics when possible |
Using Parameters<typeof fn>[0] | Type the parameter directly in function |
| Not handling union exhaustiveness | Use never checks in switch/if-else |
value as const satisfies Type | Use satisfies Type then as const if needed |
Delegation
- Pattern discovery: Use
Exploreagent to find existing patterns in codebase - Type error debugging: Use
Taskagent for multi-step type resolution - Code review: Delegate to
code-reviewerskill for type safety audit
References
- Type utilities (Pick, Omit, Partial, Required, Record, Extract, Exclude, ReturnType, Parameters, Awaited)
- Generics (constraints, inference, default parameters, variance)
- Type guards and narrowing (typeof, instanceof, in, custom guards, assertion functions)
- Discriminated unions (exhaustive checking, never type, tagged unions)
- Conditional types (distributive conditionals, infer keyword, type-level logic)
- Mapped types (key remapping, template literals, modifiers)
- Strict mode patterns (noUncheckedIndexedAccess, exactOptionalPropertyTypes, const assertions, satisfies)
- Module patterns (inline type imports, declaration files, module augmentation, ambient types)
- Modern idioms (eslint-plugin-unicorn patterns, modern array/string/DOM APIs, ES modules)
Conditional Types
Conditional types enable type-level if-else logic using the extends keyword.
Basic Conditional Types
Use T extends U ? X : Y for type-level conditionals:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;
type B = IsString<number>;
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null>;
type Flatten<T> = T extends unknown[] ? T[0] : T;
type D = Flatten<string[]>;
type E = Flatten<number>;Conditional types are evaluated when the type parameter is known.
Distributive Conditional Types
Conditional types distribute over unions:
type ToArray<T> = T extends unknown ? T[] : never;
type F = ToArray<string | number>;
type Filter<T, U> = T extends U ? T : never;
type G = Filter<'a' | 'b' | 'c' | 'd', 'a' | 'c'>;
type NonNullable<T> = T extends null | undefined ? never : T;
type H = NonNullable<string | null | number | undefined>;When T is a union, the conditional type is applied to each member separately.
Preventing Distribution
Use square brackets to prevent distribution:
type ToArray<T> = [T] extends [unknown] ? T[] : never;
type I = ToArray<string | number>;
type IsUnion<T> = [T] extends [T] ? false : true;
type J = IsUnion<string>;
type K = IsUnion<string | number>;Wrapping types in tuples disables distributive behavior.
infer Keyword
Extract types from other types with infer:
type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
type L = ReturnType<() => string>;
type Parameters<T> = T extends (...args: infer P) => unknown ? P : never;
type M = Parameters<(a: string, b: number) => void>;
type Unpromisify<T> = T extends Promise<infer U> ? U : T;
type N = Unpromisify<Promise<string>>;
type O = Unpromisify<number>;infer R declares a type variable within the conditional type.
Multiple infer
Use multiple infer declarations:
type FirstArg<T> = T extends (first: infer F, ...rest: unknown[]) => unknown
? F
: never;
type P = FirstArg<(a: string, b: number) => void>;
type SecondArg<T> = T extends (
first: unknown,
second: infer S,
...rest: unknown[]
) => unknown
? S
: never;
type Q = SecondArg<(a: string, b: number, c: boolean) => void>;
type Awaited<T> =
T extends Promise<infer U>
? U extends Promise<infer V>
? Awaited<V>
: U
: T;
type R = Awaited<Promise<Promise<string>>>;Multiple infer declarations enable complex type extraction.
Conditional Chains
Chain conditional types for complex logic:
type TypeName<T> = T extends string
? 'string'
: T extends number
? 'number'
: T extends boolean
? 'boolean'
: T extends undefined
? 'undefined'
: T extends Function
? 'function'
: 'object';
type S = TypeName<string>;
type T = TypeName<() => void>;
type IsArray<T> = T extends unknown[]
? true
: T extends readonly unknown[]
? true
: false;
type U = IsArray<string[]>;
type V = IsArray<readonly number[]>;Conditional chains behave like nested if-else statements.
Type-Level Utilities
Build reusable type utilities:
type GetProperty<T, K> = K extends keyof T ? T[K] : never;
type W = GetProperty<{ a: string; b: number }, 'a'>;
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];
type X = RequiredKeys<{ a: string; b?: number }>;
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];
type Y = OptionalKeys<{ a: string; b?: number }>;Combine conditional types with mapped types for powerful transformations.
Function Overload Resolution
Extract specific overload signatures:
type OverloadedFunction = {
(a: string): string;
(a: number): number;
(a: boolean): boolean;
};
type GetOverload<T, Args extends unknown[]> = T extends {
(...args: infer P): infer R;
}
? P extends Args
? R
: never
: never;
type Z = GetOverload<OverloadedFunction, [string]>;Conditional types can narrow overloaded function signatures.
Recursive Conditional Types
Recursively unwrap nested types:
type DeepAwaited<T> = T extends Promise<infer U> ? DeepAwaited<U> : T;
type AA = DeepAwaited<Promise<Promise<Promise<string>>>>;
type DeepReadonly<T> = T extends object
? {
readonly [K in keyof T]: DeepReadonly<T[K]>;
}
: T;
type AB = DeepReadonly<{ a: { b: { c: string } } }>;
type DeepPartial<T> = T extends object
? {
[K in keyof T]?: DeepPartial<T[K]>;
}
: T;
type AC = DeepPartial<{ a: { b: { c: string } } }>;Recursive conditional types handle arbitrarily nested structures.
Type-Level Equality
Check if two types are equal:
type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? true
: false;
type AD = Equals<string, string>;
type AE = Equals<string, number>;
type IsAny<T> = 0 extends 1 & T ? true : false;
type AF = IsAny<any>;
type AG = IsAny<unknown>;Type-level equality is useful for testing and advanced type transformations.
Conditional Type Constraints
Constrain conditional types with extends:
type ExtractArrayElement<T> = T extends (infer E)[] ? E : never;
type AH = ExtractArrayElement<string[]>;
type UnwrapPromise<T extends Promise<unknown>> =
T extends Promise<infer U> ? U : never;
type AI = UnwrapPromise<Promise<string>>;
type GetKeys<T extends object> = keyof T;
type AJ = GetKeys<{ a: string; b: number }>;Constraints ensure conditional types are only applied to valid inputs.
Real-World Example: Type-Safe Event Emitter
type EventMap = {
click: { x: number; y: number };
scroll: { top: number; left: number };
resize: { width: number; height: number };
};
type EventHandler<T> = (payload: T) => void;
class TypedEmitter<Events extends Record<string, unknown>> {
private handlers = new Map<keyof Events, Set<EventHandler<unknown>>>();
on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler as EventHandler<unknown>);
}
emit<K extends keyof Events>(event: K, payload: Events[K]) {
const handlers = this.handlers.get(event);
if (!handlers) return;
for (const handler of handlers) {
handler(payload);
}
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on('click', ({ x, y }) => console.log(x, y));
emitter.emit('click', { x: 10, y: 20 });Conditional types enable fully type-safe event systems.
Discriminated Unions
Discriminated unions (tagged unions) use a common property with literal types to enable exhaustive type checking.
Basic Discriminated Unions
Add a type or kind property to distinguish union members:
type Success = { ok: true; data: string };
type Failure = { ok: false; error: string };
type Result = Success | Failure;
function handleResult(result: Result) {
if (result.ok) {
console.log(result.data);
} else {
console.error(result.error);
}
}
type Circle = { kind: 'circle'; radius: number };
type Rectangle = { kind: 'rectangle'; width: number; height: number };
type Triangle = { kind: 'triangle'; base: number; height: number };
type Shape = Circle | Rectangle | Triangle;
function area(shape: Shape): number {
if (shape.kind === 'circle') {
return Math.PI * shape.radius ** 2;
}
if (shape.kind === 'rectangle') {
return shape.width * shape.height;
}
return (shape.base * shape.height) / 2;
}The discriminant property must be a literal type (string, number, boolean, null, undefined).
Switch Statement Exhaustiveness
Use switch for cleaner exhaustive checks:
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; value: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'increment':
return state + 1;
case 'decrement':
return state - 1;
case 'reset':
return action.value;
}
}
type Status = 'idle' | 'loading' | 'success' | 'error';
function statusColor(status: Status): string {
switch (status) {
case 'idle':
return 'gray';
case 'loading':
return 'blue';
case 'success':
return 'green';
case 'error':
return 'red';
}
}TypeScript ensures all cases are handled when there's no default case.
Never Type for Exhaustiveness
Use never to catch unhandled cases:
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${JSON.stringify(value)}`);
}
type Event = { type: 'click' } | { type: 'scroll' } | { type: 'resize' };
function handleEvent(event: Event) {
switch (event.type) {
case 'click':
return 'Clicked';
case 'scroll':
return 'Scrolled';
case 'resize':
return 'Resized';
default:
return assertNever(event);
}
}
function area(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
return assertNever(shape);
}
}Adding a new union member without handling it in the switch will cause a type error.
Nested Discriminated Unions
Discriminated unions can be nested:
type Loading = { status: 'loading' };
type Success = { status: 'success'; data: { user: User } | { users: User[] } };
type Error = { status: 'error'; error: string };
type State = Loading | Success | Error;
function render(state: State) {
if (state.status === 'loading') {
return 'Loading...';
}
if (state.status === 'error') {
return state.error;
}
if ('user' in state.data) {
return state.data.user.name;
}
return `${state.data.users.length} users`;
}Use multiple discriminants for deeply nested structures.
Discriminating with Boolean
Boolean discriminants work for two-member unions:
type Unauthenticated = { authenticated: false };
type Authenticated = { authenticated: true; user: User };
type Auth = Unauthenticated | Authenticated;
function render(auth: Auth) {
if (auth.authenticated) {
return `Welcome, ${auth.user.name}`;
}
return 'Please log in';
}
type Empty = { hasValue: false };
type Filled = { hasValue: true; value: string };
type Optional = Empty | Filled;
function getValue(opt: Optional): string {
if (opt.hasValue) {
return opt.value;
}
return 'N/A';
}For unions with more than two members, use string literals instead.
Discriminated Unions in Reducers
Common pattern in state management:
type State = { count: number; message: string };
type Action =
| { type: 'increment'; by?: number }
| { type: 'decrement'; by?: number }
| { type: 'reset'; to: number }
| { type: 'setMessage'; message: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + (action.by ?? 1) };
case 'decrement':
return { ...state, count: state.count - (action.by ?? 1) };
case 'reset':
return { ...state, count: action.to };
case 'setMessage':
return { ...state, message: action.message };
}
}
const newState = reducer(
{ count: 0, message: '' },
{ type: 'increment', by: 5 },
);Each action has a type discriminant and type-specific payload.
Generic Discriminated Unions
Make discriminated unions generic:
type Ok<T> = { ok: true; value: T };
type Err<E> = { ok: false; error: E };
type Result<T, E = Error> = Ok<T> | Err<E>;
function parseJSON(json: string): Result<unknown, SyntaxError> {
try {
return { ok: true, value: JSON.parse(json) };
} catch (error) {
return { ok: false, error: error as SyntaxError };
}
}
function map<T, U, E>(result: Result<T, E>, fn: (value: T) => U): Result<U, E> {
if (result.ok) {
return { ok: true, value: fn(result.value) };
}
return result;
}
const result = parseJSON('{"name":"Alice"}');
const mapped = map(result, (data) => (data as { name: string }).name);Generic discriminated unions enable type-safe error handling patterns.
Multiple Discriminants
Use multiple properties for complex discrimination:
type Event =
| { category: 'user'; action: 'login'; userId: string }
| { category: 'user'; action: 'logout'; userId: string }
| { category: 'system'; action: 'error'; message: string }
| { category: 'system'; action: 'info'; message: string };
function logEvent(event: Event) {
if (event.category === 'user') {
if (event.action === 'login') {
console.log(`User ${event.userId} logged in`);
} else {
console.log(`User ${event.userId} logged out`);
}
} else {
if (event.action === 'error') {
console.error(event.message);
} else {
console.info(event.message);
}
}
}Multiple discriminants allow hierarchical narrowing.
Const Assertions in Unions
Use as const for literal type inference:
const success = { ok: true, data: 'hello' } as const;
const failure = { ok: false, error: 'oops' } as const;
type Result = typeof success | typeof failure;
function handleResult(result: Result) {
if (result.ok) {
console.log(result.data);
} else {
console.error(result.error);
}
}
const actions = {
increment: { type: 'increment' } as const,
decrement: { type: 'decrement' } as const,
reset: (to: number) => ({ type: 'reset', to }) as const,
};
type Action = ReturnType<(typeof actions)[keyof typeof actions]>;Const assertions ensure discriminants are literal types.
Avoiding Common Pitfalls
Keep the discriminant property consistent:
type Event = { type: 'click'; x: number } | { kind: 'scroll'; y: number };
function handle(event: Event) {
if ('type' in event) {
console.log(event.x);
}
}
type EventFixed = { type: 'click'; x: number } | { type: 'scroll'; y: number };
function handleFixed(event: EventFixed) {
if (event.type === 'click') {
console.log(event.x);
} else {
console.log(event.y);
}
}Use the same discriminant property name across all union members.
Pattern Matching Libraries
TypeScript doesn't have built-in pattern matching, but libraries provide it:
import { match } from 'ts-pattern';
type Response =
| { status: 'loading' }
| { status: 'success'; data: string }
| { status: 'error'; error: Error };
const result = match(response)
.with({ status: 'loading' }, () => 'Loading...')
.with({ status: 'success' }, ({ data }) => `Data: ${data}`)
.with({ status: 'error' }, ({ error }) => `Error: ${error.message}`)
.exhaustive();Pattern matching libraries provide exhaustiveness checking and cleaner syntax.
Generics
Generics enable code reuse across different types while maintaining type safety.
Basic Generics
Generic functions and types with type parameters:
function identity<T>(value: T): T {
return value;
}
const num = identity(42);
const str = identity('hello');
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
type Box<T> = {
value: T;
};
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: 'hello' };TypeScript infers the type parameter from usage. Explicit type arguments are rarely needed.
Generic Constraints
Restrict type parameters with extends:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: '1', name: 'Alice' };
const id = getProperty(user, 'id');
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest('hello', 'world');
longest([1, 2], [3, 4, 5]);
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}Always constrain generics when you need to access specific properties or methods.
Multiple Type Parameters
Use multiple generics for complex relationships:
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
const result: U[] = [];
for (const item of arr) {
result.push(fn(item));
}
return result;
}
const numbers = [1, 2, 3];
const strings = map(numbers, (n) => n.toString());
type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E };
function tryParse(json: string): Result<unknown, SyntaxError> {
try {
return { ok: true, value: JSON.parse(json) };
} catch (error) {
return { ok: false, error: error as SyntaxError };
}
}The second generic E = Error has a default type, making it optional.
Default Type Parameters
Provide default values for type parameters:
type ApiResponse<T = unknown, E = Error> = {
data?: T;
error?: E;
status: number;
};
const response1: ApiResponse = { status: 200 };
const response2: ApiResponse<User> = { data: user, status: 200 };
interface State<T = string> {
value: T;
update: (newValue: T) => void;
}
const stringState: State = { value: '', update: () => {} };
const numberState: State<number> = { value: 0, update: () => {} };Defaults make generics optional, improving usability for common cases.
Generic Inference
Let TypeScript infer types from function arguments:
function createPair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const pair = createPair('hello', 42);
function filterNotNull<T>(arr: (T | null)[]): T[] {
const result: T[] = [];
for (const item of arr) {
if (item !== null) {
result.push(item);
}
}
return result;
}
const numbers = filterNotNull([1, null, 2, null, 3]);
function promisify<T extends unknown[], R>(
fn: (...args: T) => R,
): (...args: T) => Promise<R> {
return async (...args) => fn(...args);
}Inference works best when the generic appears in function parameters, not just the return type.
Conditional Generic Constraints
Use conditional types with generics:
type Flatten<T> = T extends unknown[] ? T[0] : T;
function flatten<T>(value: T): Flatten<T> {
return (Array.isArray(value) ? value[0] : value) as Flatten<T>;
}
const num = flatten(42);
const str = flatten(['hello']);
type Unpromisify<T> = T extends Promise<infer U> ? U : T;
function resolve<T>(value: T): Unpromisify<T> {
return (
value instanceof Promise ? value : Promise.resolve(value)
) as Unpromisify<T>;
}Conditional types within generics enable complex type transformations.
Generic Classes
Classes with type parameters:
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
peek(): T | undefined {
return this.items[0];
}
}
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
class Result<T, E extends Error = Error> {
private constructor(
public readonly ok: boolean,
public readonly value?: T,
public readonly error?: E,
) {}
static success<T>(value: T): Result<T> {
return new Result(true, value);
}
static failure<E extends Error>(error: E): Result<never, E> {
return new Result(false, undefined, error);
}
}Generic classes are useful for data structures and container types.
Variance
TypeScript automatically infers variance for generics:
type Producer<out T> = () => T;
type Consumer<in T> = (value: T) => void;
type Mutable<in out T> = {
get: () => T;
set: (value: T) => void;
};
interface ReadonlyBox<out T> {
readonly value: T;
}
interface WriteableBox<in T> {
set(value: T): void;
}
interface Box<T> {
value: T;
}out T(covariance): Type can be returned but not accepted as parameterin T(contravariance): Type can be accepted but not returnedin out T(invariance): Type can be both accepted and returned
Most generic types are covariant by default.
Generic Utility Functions
Reusable type-safe utilities:
function groupBy<T, K extends string | number>(
arr: T[],
getKey: (item: T) => K,
): Record<K, T[]> {
const result = {} as Record<K, T[]>;
for (const item of arr) {
const key = getKey(item);
if (!result[key]) {
result[key] = [];
}
result[key].push(item);
}
return result;
}
const users = [
{ id: '1', role: 'admin' },
{ id: '2', role: 'user' },
];
const byRole = groupBy(users, (u) => u.role);
function memoize<Args extends unknown[], Return>(
fn: (...args: Args) => Return,
): (...args: Args) => Return {
const cache = new Map<string, Return>();
return (...args) => {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key)!;
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}Generics enable type-safe higher-order functions.
Const Type Parameters
Use const modifier on type parameters for narrowest inference without as const:
function getNames<const T extends readonly string[]>(names: T): T {
return names;
}
// Inferred: readonly ["Alice", "Bob"]
const names = getNames(['Alice', 'Bob']);
type HasNames = { names: readonly string[] };
function getNamesExactly<const T extends HasNames>(arg: T): T['names'] {
return arg.names;
}
// Inferred: readonly ["Alice", "Bob", "Eve"]
const result = getNamesExactly({ names: ['Alice', 'Bob', 'Eve'] });The const modifier only applies to literal values passed directly. Variables already have widened types and are unaffected. The constraint must use readonly for arrays to benefit.
Avoiding Manual Type Arguments
Prefer inference over explicit type parameters:
const arr1 = Array<number>();
const arr2 = [] as number[];
const arr3: number[] = [];
function fetch<T>(url: string): Promise<T> {
return fetch(url).then((r) => r.json());
}
const user1 = await fetch<User>('/api/user');
async function fetchTyped(url: string): Promise<User> {
const response = await fetch(url);
return response.json();
}
const user2 = await fetchTyped('/api/user');Type the function return instead of making the caller specify type arguments.
Mapped Types
Mapped types transform all properties of an existing type.
Basic Mapped Types
Iterate over keys with in keyof:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type User = {
id: string;
name: string;
};
type ReadonlyUser = Readonly<User>;
type Partial<T> = {
[K in keyof T]?: T[K];
};
type PartialUser = Partial<User>;
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
type NullableUser = Nullable<User>;Mapped types iterate over each property of the source type.
Adding and Removing Modifiers
Use + and - to add or remove readonly and ?:
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type Required<T> = {
[K in keyof T]-?: T[K];
};
type Optional<T> = {
+readonly [K in keyof T]?: T[K];
};
type Config = {
readonly host: string;
readonly port?: number;
};
type MutableConfig = Mutable<Config>;
type RequiredConfig = Required<Config>;Omit the + or - prefix to leave modifiers unchanged.
Key Remapping with as
Rename keys using as and template literals:
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};
type UserSetters = Setters<User>;
type Events<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void;
};
type UserEvents = Events<User>;Key remapping transforms property names during iteration.
Filtering Properties
Use never to exclude keys:
type RemoveReadonly<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K];
};
type User = {
id: string;
name: string;
save: () => void;
};
type UserData = RemoveReadonly<User>;
type PickByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? K : never]: T[K];
};
type StringProps = PickByType<User, string>;
type OmitByType<T, ValueType> = {
[K in keyof T as T[K] extends ValueType ? never : K]: T[K];
};
type NonStringProps = OmitByType<User, string>;never keys are automatically removed from the resulting type.
Template Literal Types
Combine strings at the type level:
type Direction = 'top' | 'right' | 'bottom' | 'left';
type Size = 'sm' | 'md' | 'lg';
type Margin = `margin${Capitalize<Direction>}`;
type PaddingSize = `padding-${Size}`;
type CSSProperty = `${Direction}-${Size}`;
type EventName<T extends string> = `on${Capitalize<T>}`;
type Events = EventName<'click' | 'scroll' | 'resize'>;Template literal types work like template strings but at the type level.
Built-in String Manipulation
TypeScript provides built-in string utility types:
type Uppercased = Uppercase<'hello'>;
type Lowercased = Lowercase<'HELLO'>;
type Capitalized = Capitalize<'hello'>;
type Uncapitalized = Uncapitalize<'Hello'>;
type Route = '/users' | '/posts' | '/comments';
type RouteName = Capitalize<Uppercase<Route>>;These are intrinsic types implemented by the compiler.
Nested Mapped Types
Transform nested object properties:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type Config = {
server: {
host: string;
port: number;
};
cache: {
ttl: number;
};
};
type PartialConfig = DeepPartial<Config>;Recursive mapped types handle arbitrarily nested structures.
Conditional Property Types
Change property types conditionally:
type Stringify<T> = {
[K in keyof T]: T[K] extends string ? T[K] : string;
};
type Numberify<T> = {
[K in keyof T]: T[K] extends number ? T[K] : number;
};
type Promisify<T> = {
[K in keyof T]: Promise<T[K]>;
};
type AsyncUser = Promisify<User>;
type MaybeNull<T> = {
[K in keyof T]: T[K] | null;
};Combine mapped types with conditional types for powerful transformations.
Union to Intersection
Convert union types to intersections:
type UnionToIntersection<U> = (
U extends unknown ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;
type Union = { a: string } | { b: number };
type Intersection = UnionToIntersection<Union>;
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof T
? K extends keyof U
? T[K] | U[K]
: T[K]
: K extends keyof U
? U[K]
: never;
};
type A = { a: string; b: number };
type B = { b: string; c: boolean };
type C = Merge<A, B>;Advanced type manipulation for complex scenarios.
Pick and Omit Implementation
Implement utility types with mapped types:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Omit<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
};
type Record<K extends string | number | symbol, T> = {
[P in K]: T;
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};All built-in utility types are implemented with mapped types.
Real-World Example: Type-Safe Form State
type FormField<T> = {
value: T;
error?: string;
touched: boolean;
};
type FormState<T> = {
[K in keyof T]: FormField<T[K]>;
};
type User = {
name: string;
email: string;
age: number;
};
type UserFormState = FormState<User>;
const form: UserFormState = {
name: { value: '', error: undefined, touched: false },
email: { value: '', error: undefined, touched: false },
age: { value: 0, error: undefined, touched: false },
};
type FormValidators<T> = {
[K in keyof T]?: (value: T[K]) => string | undefined;
};
const validators: FormValidators<User> = {
name: (value) => (value.length < 2 ? 'Name too short' : undefined),
email: (value) => (value.includes('@') ? undefined : 'Invalid email'),
age: (value) => (value >= 18 ? undefined : 'Must be 18+'),
};Mapped types enable type-safe form handling.
Key Remapping with Union Types
Distribute key remapping over unions:
type Action =
| { type: 'increment'; by: number }
| { type: 'decrement'; by: number }
| { type: 'reset' };
type ActionCreators = {
[A in Action as A['type']]: (
...args: A extends { by: number } ? [by: number] : []
) => A;
};
const actions: ActionCreators = {
increment: (by) => ({ type: 'increment', by }),
decrement: (by) => ({ type: 'decrement', by }),
reset: () => ({ type: 'reset' }),
};Key remapping works with discriminated unions.
Combining Multiple Mapped Types
Chain mapped types for complex transformations:
type ReadonlyPartial<T> = Readonly<Partial<T>>;
type RequiredNonNullable<T> = {
[K in keyof T]-?: NonNullable<T[K]>;
};
type DeepReadonlyPartial<T> = {
readonly [K in keyof T]?: T[K] extends object
? DeepReadonlyPartial<T[K]>
: T[K];
};
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
type MutableRequired<T> = Mutable<Required<T>>;Compose utility types to express complex requirements.
Modern Idioms
Modern JavaScript/TypeScript idioms commonly enforced by eslint-plugin-unicorn. Prefer these patterns over legacy alternatives.
Arrays
// Use for-of instead of reduce
array.reduce((acc, item) => acc + item, 0); // Avoid
let sum = 0;
for (const item of array) sum += item; // Preferred
// Use for-of instead of forEach
array.forEach((item) => console.log(item)); // Avoid
for (const item of array) console.log(item); // Preferred
// Use for-of instead of traditional for loops
for (let i = 0; i < array.length; i++) {} // Avoid
for (const item of array) {
} // Preferred
for (const [index, item] of array.entries()) {
} // When index needed
// Use Array.from instead of new Array
new Array(10); // Avoid
Array.from({ length: 10 }); // PreferredModern Array Methods
array.at(-1); // not array[array.length - 1]
array.includes(x); // not array.indexOf(x) !== -1
array.find((x) => x.id === id); // not array.filter(...)[0]
array.some((x) => x.valid); // not array.filter(...).length > 0
array.flat(); // not [].concat(...array)
array.flatMap((x) => x.items); // not array.map(...).flat()
array.toSorted(); // not [...array].sort()
array.toReversed(); // not [...array].reverse()Strings
str.replaceAll('a', 'b'); // not str.replace(/a/g, 'b')
str.slice(1, 3); // not str.substr() or str.substring()
str.startsWith('x'); // not str.indexOf('x') === 0
str.endsWith('x'); // not str.slice(-1) === 'x'
str.trimStart(); // not str.trimLeft()
str.trimEnd(); // not str.trimRight()
str.codePointAt(0); // not str.charCodeAt(0)
str.at(-1); // not str.charAt(str.length - 1)Errors
// Always use "throw new Error" with a message
throw new Error('Something went wrong'); // Correct
throw Error('msg'); // Missing "new"
throw 'error'; // Not an Error object
throw new Error(); // Missing message
// Catch variable must be named "error"
catch (error) {} // Correct
catch (e) {} // Avoid
catch (err) {} // Avoid
// Use TypeError for type validation
if (typeof x !== 'string') throw new TypeError('Expected string');ES Modules
// Use ES module syntax
import x from 'module'; // Correct
export { x }; // Correct
const x = require('module'); // Avoid CommonJS
module.exports = x; // Avoid CommonJS
// Use node: protocol for Node.js builtins
import fs from 'node:fs'; // Correct
import path from 'node:path'; // Correct
import fs from 'fs'; // Missing protocol
// Use globalThis instead of environment-specific globals
globalThis.setTimeout; // Correct
window.setTimeout; // Browser-specific
global.setTimeout; // Node-specificConditionals
// No nested ternaries
const x = a ? (b ? 1 : 2) : 3; // Avoid — use if-else or extract to functions
// Prefer nullish coalescing over ternary
const x = a ?? b; // not: a !== null ? a : b
const x = a || b; // not: a ? a : b
// No negated conditions with else
if (!condition) {
/* ... */
} else {
/* ... */
} // Avoid
if (condition) {
/* ... */
} else {
/* ... */
} // Preferred — flip the conditionFunctions and Classes
// No static-only classes — use plain objects or functions
class Utils {
static helper() {}
} // Avoid
function helper() {} // Preferred
// Use class fields for initialization
class Foo {
bar = 'value'; // Preferred over constructor assignment
}DOM
// Modern DOM APIs
element.append(child); // not appendChild
element.remove(); // not parent.removeChild(element)
element.querySelector('.x'); // not getElementById or getElementsByClassName
element.closest('.x'); // not manual parent traversal
element.dataset.value; // not getAttribute('data-value')
element.addEventListener('click', fn); // not onclick = fn
element.classList.toggle('active'); // not manual add/remove
// Use KeyboardEvent.key
event.key === 'Enter'; // not event.keyCode === 13Numbers
Number.isNaN(x); // not isNaN(x)
Number.isFinite(x); // not isFinite(x)
Number.parseInt(x); // not parseInt(x)
Math.trunc(x); // not x | 0 or ~~x
// Use numeric separators for readability
const billion = 1_000_000_000;Miscellaneous
// Explicit length check
if (array.length > 0) {
} // Preferred
if (array.length) {
} // Avoid — implicit boolean coercion
// Use Set for repeated has() checks
const set = new Set(array);
set.has(x);
// Re-export directly
export { x } from './module'; // not import then export
// Optional catch binding when error unused
try {
/* ... */
} catch {} // Omit variable if unused
// Avoid process.exit() — throw errors instead
// Avoid document.cookie — use a cookie libraryQuick Reference
| Legacy Pattern | Modern Replacement |
|---|---|
array.reduce() | for-of loop |
array.forEach() | for-of loop |
for (let i = 0; ...) | for-of or array.entries() |
array[array.length - 1] | array.at(-1) |
[...array].sort() | array.toSorted() |
str.replace(/x/g, 'y') | str.replaceAll('x', 'y') |
import fs from 'fs' | import fs from 'node:fs' |
catch (e) | catch (error) |
throw 'error' | throw new Error('message') |
isNaN(x) | Number.isNaN(x) |
element.appendChild(x) | element.append(x) |
event.keyCode === 13 | event.key === 'Enter' |
Module Patterns
TypeScript module patterns for organizing types, augmenting third-party libraries, and managing ambient declarations.
Inline Type Imports
Import types explicitly with type keyword:
import { type User, createUser } from './user';
import { type Config } from './config';
function processUser(user: User) {
console.log(user.name);
}
import { type FC } from 'react';
const Button: FC<{ label: string }> = ({ label }) => {
return <button>{label}</button>;
};Inline type imports are removed from compiled JavaScript, reducing bundle size.
Type-Only Imports
Import only types:
import type { User, Post, Comment } from './types';
function renderUser(user: User) {
console.log(user.name);
}
import type * as Types from './types';
function renderPost(post: Types.Post) {
console.log(post.title);
}import type ensures the import is only used for types, never values.
Type-Only Exports
Export types explicitly:
export type { User, Post } from './types';
export type { Config as AppConfig } from './config';
type User = { id: string; name: string };
export type { User };
export { createUser };
export type { User };Type-only exports clarify which exports are types vs values.
Declaration Files
Define types without implementation:
declare module 'my-library' {
export function doSomething(input: string): string;
export class MyClass {
constructor(name: string);
getName(): string;
}
export const VERSION: string;
}
declare module '*.svg' {
const content: string;
export default content;
}
declare module '*.css' {
const classes: Record<string, string>;
export default classes;
}Declaration files (.d.ts) contain only type information.
Module Augmentation
Extend third-party module types:
import 'express';
declare module 'express' {
interface Request {
user?: {
id: string;
name: string;
};
}
}
import 'fastify';
declare module 'fastify' {
interface FastifyRequest {
user: User;
}
}
import '@tanstack/react-query';
declare module '@tanstack/react-query' {
interface Register {
defaultError: AxiosError;
}
}Module augmentation adds properties to existing types from external libraries.
Global Augmentation
Add types to the global scope:
declare global {
interface Window {
ENV: {
API_URL: string;
VERSION: string;
};
}
const API_URL: string;
namespace NodeJS {
interface ProcessEnv {
DATABASE_URL: string;
API_KEY: string;
}
}
}
export {};
declare global {
type UUID = string;
type Timestamp = number;
}
export {};Use declare global to add types to the global scope. The export {} makes the file a module.
Ambient Declarations
Declare types for untyped libraries:
declare const VERSION: string;
declare const BUILD_TIME: number;
declare function gtag(
command: 'config' | 'event',
targetId: string,
config?: Record<string, unknown>,
): void;
declare class Analytics {
constructor(apiKey: string);
track(event: string, properties?: Record<string, unknown>): void;
identify(userId: string, traits?: Record<string, unknown>): void;
}
declare namespace Stripe {
interface StripeStatic {
(key: string): StripeInstance;
}
interface StripeInstance {
createToken(element: Element): Promise<TokenResponse>;
}
}Ambient declarations describe the shape of external code without providing implementation.
Triple-Slash Directives
Reference other declaration files:
/// <reference types="node" />
/// <reference path="./custom.d.ts" />
/// <reference lib="es2020" />
/// <reference lib="dom" />
export function readFile(path: string): Buffer;Triple-slash directives are legacy syntax. Prefer import statements.
UMD Modules
Declare types for UMD libraries:
export as namespace MyLibrary;
export function doSomething(input: string): string;
export class MyClass {
constructor(name: string);
}
declare global {
interface Window {
MyLibrary: typeof import('./index');
}
}UMD modules work in both module and global contexts.
Namespace Augmentation
Extend namespaces:
declare namespace MyNamespace {
interface Config {
apiUrl: string;
}
}
declare namespace MyNamespace {
interface Config {
timeout: number;
}
function init(config: Config): void;
}
const config: MyNamespace.Config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};Multiple declare namespace declarations merge into a single namespace.
Exporting from Declaration Files
Export types from .d.ts files:
export type User = {
id: string;
name: string;
};
export interface Post {
id: string;
title: string;
authorId: string;
}
export declare function createUser(name: string): User;
export declare class Database {
constructor(url: string);
query(sql: string): Promise<unknown[]>;
}Declaration files can export types, interfaces, and declare functions/classes.
Module Resolution
Configure module resolution:
{
"compilerOptions": {
"moduleResolution": "bundler",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"~/*": ["app/*"]
}
}
}Use path mapping for cleaner imports:
import { User } from '@/types/user';
import { config } from '~/config';CommonJS Interop
Import CommonJS modules:
import express from 'express';
import * as express from 'express';
const express = require('express');
export = MyClass;
import MyClass = require('./my-class');Use esModuleInterop for better CommonJS/ESM compatibility:
{
"compilerOptions": {
"esModuleInterop": true
}
}Real-World Example: Typed Environment Variables
declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
DATABASE_URL: string;
API_KEY: string;
PORT?: string;
}
}
}
export {};
const dbUrl: string = process.env.DATABASE_URL;
const port: number = Number.parseInt(process.env.PORT ?? '3000');
const isDev: boolean = process.env.NODE_ENV === 'development';Type-safe environment variables prevent runtime errors.
Package Exports Field
Define package entry points:
{
"name": "my-library",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.mjs"
}
}
}The exports field defines how your package can be imported.
Type-Only Packages
Publish types separately:
{
"name": "@types/my-library",
"version": "1.0.0",
"types": "index.d.ts",
"files": ["*.d.ts"]
}Create type-only packages for untyped JavaScript libraries.
Declaration Maps
Generate source maps for types:
{
"compilerOptions": {
"declaration": true,
"declarationMap": true
}
}Declaration maps enable "Go to Definition" for compiled libraries.
Composite Projects
Split large codebases into projects:
{
"compilerOptions": {
"composite": true,
"rootDir": ".",
"outDir": "dist"
},
"references": [{ "path": "../shared" }]
}Use project references for incremental builds:
import { User } from '@shared/types';
export function processUser(user: User) {
console.log(user.name);
}Module Wildcards
Handle dynamic imports:
declare module 'virtual:*' {
const content: string;
export default content;
}
declare module '*.svg?component' {
import { type FC } from 'react';
const Component: FC;
export default Component;
}
declare module '*.module.css' {
const classes: Record<string, string>;
export default classes;
}
declare module 'data:*' {
const content: string;
export default content;
}Module wildcards handle build tool-specific imports.
Strict Mode Patterns
Strict mode TypeScript catches more bugs at compile time with additional type checking.
Strict Flag
Enable all strict checks with strict: true:
{
"compilerOptions": {
"strict": true
}
}This enables:
noImplicitAny- No implicitanytypesstrictNullChecks-nullandundefinedmust be handled explicitlystrictFunctionTypes- Stricter function parameter checkingstrictBindCallApply- Type-checkbind,call,applystrictPropertyInitialization- Class properties must be initializednoImplicitThis-thismust be typed explicitlyalwaysStrict- Emit"use strict"in generated codeuseUnknownInCatchVariables- Catch variables typed asunknown
noUncheckedIndexedAccess
Array and object access returns T | undefined:
const arr: number[] = [1, 2, 3];
const first = arr[0];
const value = arr[10];
if (value !== undefined) {
console.log(value * 2);
}
const obj: Record<string, string> = { a: 'hello' };
const a = obj['a'];
const b = obj['b'];
if (b) {
console.log(b.toUpperCase());
}Without this flag, arr[10] would be typed as number even though it's undefined.
Enable with:
{
"compilerOptions": {
"noUncheckedIndexedAccess": true
}
}exactOptionalPropertyTypes
Distinguish between undefined value and missing property:
type User = {
name: string;
email?: string;
};
const user1: User = { name: 'Alice' };
const user2: User = { name: 'Bob', email: undefined };
const user3: User = { name: 'Charlie', email: 'charlie@example.com' };
function hasEmail(user: User): boolean {
return 'email' in user;
}Without this flag, email: undefined is allowed. With it, optional properties cannot be explicitly set to undefined.
Enable with:
{
"compilerOptions": {
"exactOptionalPropertyTypes": true
}
}Const Assertions
Use as const for narrowest possible types:
const config = {
host: 'localhost',
port: 3000,
} as const;
type Config = typeof config;
const routes = ['/', '/about', '/contact'] as const;
type Route = (typeof routes)[number];
const status = 'success' as const;
const tuple = [1, 'hello', true] as const;
const nested = {
server: {
host: 'localhost',
},
cache: {
ttl: 3600,
},
} as const;as const makes all properties readonly and infers literal types instead of widened types.
Const Assertions vs Readonly
Const assertions are deeper than Readonly:
const obj1: Readonly<{ a: string }> = { a: 'hello' };
const obj2 = { a: 'hello' } as const;
const nested1: Readonly<{ a: { b: string } }> = { a: { b: 'hello' } };
nested1.a.b = 'world';
const nested2 = { a: { b: 'hello' } } as const;as const makes all nested properties readonly recursively.
Satisfies Operator
Type-check without widening:
type Color = 'red' | 'green' | 'blue' | { r: number; g: number; b: number };
const palette = {
primary: 'red',
secondary: { r: 0, g: 255, b: 0 },
} satisfies Record<string, Color>;
const primary = palette.primary;
type Route = { path: string; method: 'GET' | 'POST' };
const routes = {
home: { path: '/', method: 'GET' },
createUser: { path: '/users', method: 'POST' },
} satisfies Record<string, Route>;
const homeMethod = routes.home.method;satisfies ensures the value matches the type without changing the inferred type.
Satisfies vs Type Annotation
Type annotation widens, satisfies preserves narrow types:
type Route = { path: string; handler: () => void };
const routes1: Record<string, Route> = {
home: { path: '/', handler: () => console.log('home') },
};
const homePath = routes1.home.path;
const routes2 = {
home: { path: '/', handler: () => console.log('home') },
} satisfies Record<string, Route>;
const homeHandler = routes2.home.handler;Use satisfies when you want type safety without losing narrow types.
Combining Satisfies and As Const
Order matters when combining operators:
const config = {
host: 'localhost',
port: 3000,
} as const satisfies Config;
const routes = [
{ path: '/', method: 'GET' },
{ path: '/users', method: 'POST' },
] satisfies Route[] as const;Use as const satisfies Type when you want both narrowing and validation.
Unknown vs Any
Prefer unknown over any:
function processValue(value: unknown) {
if (typeof value === 'string') {
return value.toUpperCase();
}
if (typeof value === 'number') {
return value.toFixed(2);
}
throw new Error('Unsupported type');
}
function parseJSON(json: string): unknown {
return JSON.parse(json);
}
const data = parseJSON('{"name":"Alice"}');
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log((data as { name: string }).name);
}unknown requires type narrowing before use. any bypasses all type checking.
Non-null Assertion Operator
Use ! sparingly to assert non-null:
function getElement(id: string): HTMLElement | null {
return document.getElementById(id);
}
const element = getElement('root')!;
element.textContent = 'Hello';
const arr: number[] = [1, 2, 3];
const first = arr[0]!;
const user: User | undefined = getUser();
console.log(user!.name);Only use ! when you're certain the value is non-null. Prefer explicit null checks.
Definite Assignment Assertion
Tell TypeScript a property is initialized:
class Component {
element!: HTMLElement;
constructor(id: string) {
this.initialize(id);
}
initialize(id: string) {
this.element = document.getElementById(id) as HTMLElement;
}
}
let x!: number;
initialize();
console.log(x * 2);
function initialize() {
x = 42;
}Use ! after property name to skip initialization checks. Ensure the property is actually initialized.
Template Literal Types with Strict Mode
Combine template literals with strict checks:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = `/${string}`;
type Endpoint = `${HttpMethod} ${Route}`;
const endpoint1: Endpoint = 'GET /users';
type CSSUnit = `${number}${'px' | 'em' | 'rem' | '%'}`;
const width: CSSUnit = '100px';
type EventName<T extends string> = `on${Capitalize<T>}`;
type Handler<T extends string> = Record<EventName<T>, () => void>;
const handlers: Handler<'click' | 'scroll'> = {
onClick: () => {},
onScroll: () => {},
};Template literal types enable compile-time string validation.
Real-World Example: Type-Safe Configuration
type Environment = 'development' | 'staging' | 'production';
type Config = {
env: Environment;
api: {
baseUrl: string;
timeout: number;
};
features: {
analytics: boolean;
darkMode: boolean;
};
};
const config = {
env: 'production',
api: {
baseUrl: 'https://api.example.com',
timeout: 5000,
},
features: {
analytics: true,
darkMode: true,
},
} as const satisfies Config;
type ConfigEnv = typeof config.env;
function getApiUrl(env: ConfigEnv): string {
return config.api.baseUrl;
}Combine strict mode features for fully type-safe configuration.
NoImplicitAny
Avoid implicit any types:
function add(a, b) {
return a + b;
}
function addTyped(a: number, b: number): number {
return a + b;
}
const values = [1, 2, 3];
const doubled = values.map((x) => x * 2);
const user = { name: 'Alice', age: 30 };
const keys = Object.keys(user);
const name = user[keys[0]];Always type function parameters explicitly. Array methods often infer correctly.
StrictNullChecks
Handle null and undefined explicitly:
function getLength(str: string | null): number {
if (str === null) {
return 0;
}
return str.length;
}
function getUser(): User | undefined {
return undefined;
}
const user = getUser();
if (user) {
console.log(user.name);
}
const value: string | null = getValue();
const length = value?.length ?? 0;strictNullChecks makes null and undefined distinct types that must be handled.
Type Guards and Narrowing
Type guards narrow union types to specific types based on runtime checks.
typeof Guards
Check primitive types with typeof:
function processValue(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function formatValue(value: string | number | boolean) {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number') {
return value.toString();
}
return value ? 'yes' : 'no';
}typeof returns: 'string', 'number', 'boolean', 'symbol', 'undefined', 'object', 'function', 'bigint'.
Note: typeof null returns 'object' due to a JavaScript quirk.
instanceof Guards
Check class instances with instanceof:
class NetworkError extends Error {
constructor(
message: string,
public statusCode: number,
) {
super(message);
}
}
function handleError(error: Error) {
if (error instanceof NetworkError) {
console.error(`HTTP ${error.statusCode}: ${error.message}`);
return;
}
console.error(error.message);
}
function isDate(value: unknown): value is Date {
return value instanceof Date;
}instanceof works with class constructors, not plain object types or interfaces.
in Operator
Check for property existence:
type Dog = { bark: () => void };
type Cat = { meow: () => void };
function makeSound(animal: Dog | Cat) {
if ('bark' in animal) {
animal.bark();
} else {
animal.meow();
}
}
type Response = { data: unknown } | { error: string };
function handleResponse(response: Response) {
if ('error' in response) {
console.error(response.error);
return;
}
console.log(response.data);
}in narrows based on property presence. Works with optional properties too.
Truthiness Narrowing
TypeScript narrows based on truthiness checks:
function processUser(user: User | null | undefined) {
if (!user) {
return;
}
console.log(user.name);
}
function firstElement<T>(arr: T[]): T | undefined {
if (arr.length) {
return arr[0];
}
return undefined;
}
function processValue(value: string | null) {
if (value) {
return value.toUpperCase();
}
}Avoid truthiness checks for values that can be falsy but valid (e.g., 0, '', false).
Equality Narrowing
Check for specific values:
type Status = 'idle' | 'loading' | 'success' | 'error';
function renderStatus(status: Status) {
if (status === 'loading') {
return 'Loading...';
}
if (status === 'success') {
return 'Done!';
}
return 'Error or idle';
}
function processValue(value: string | number | null) {
if (value === null) {
return;
}
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}Equality checks narrow unions to specific literal types.
Custom Type Guards
Define type predicates for reusable narrowing:
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
typeof (value as User).id === 'string' &&
typeof (value as User).name === 'string'
);
}
function processValue(value: unknown) {
if (isUser(value)) {
console.log(value.name);
}
}
function isNotNull<T>(value: T | null): value is T {
return value !== null;
}
const users: (User | null)[] = [user1, null, user2];
const validUsers = users.filter(isNotNull);
function isError(value: unknown): value is Error {
return value instanceof Error;
}Type predicates use the value is Type syntax in the return type.
Assertion Functions
Throw if a condition is not met, narrowing the type:
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function processUser(user: User | null) {
assert(user !== null, 'User must be defined');
console.log(user.name);
}
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new TypeError('Value must be a string');
}
}
function processValue(value: unknown) {
assertIsString(value);
return value.toUpperCase();
}
function assertNonNull<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value must not be null or undefined');
}
}Assertion functions use asserts condition or asserts value is Type in the return type.
Array.isArray Narrowing
Narrow arrays with Array.isArray:
function processValue(value: string | string[]) {
if (Array.isArray(value)) {
return value.join(', ');
}
return value;
}
function flatten(value: unknown): unknown[] {
if (Array.isArray(value)) {
return value;
}
return [value];
}
function isStringArray(value: unknown): value is string[] {
return (
Array.isArray(value) && value.every((item) => typeof item === 'string')
);
}Array.isArray narrows to array types.
Nullish Checks
Check for null or undefined:
function getLength(value: string | null | undefined): number {
if (value == null) {
return 0;
}
return value.length;
}
function processUser(user: User | null | undefined) {
if (user !== null && user !== undefined) {
console.log(user.name);
}
}
const value: string | null = getValue();
if (value !== null) {
console.log(value.toUpperCase());
}Use == null to check for both null and undefined, or check explicitly.
Control Flow Analysis
TypeScript tracks narrowing through control flow:
function processValue(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}
function formatValue(value: string | null) {
if (!value) {
return 'N/A';
}
const upper = value.toUpperCase();
const trimmed = value.trim();
return `${upper} (${trimmed.length})`;
}
function handleError(error: unknown) {
if (!(error instanceof Error)) {
return;
}
console.error(error.message);
if (error instanceof NetworkError) {
console.error(`Status: ${error.statusCode}`);
}
}Type narrowing persists within the same code path.
Type Predicates vs Assertion Functions
Choose based on how you want to handle invalid values:
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
);
}
if (isUser(value)) {
console.log(value.name);
}
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new TypeError('Invalid user object');
}
}
assertIsUser(value);
console.log(value.name);Use type predicates for conditional handling. Use assertion functions when invalid input is a programming error.
Inferred Type Predicates
TypeScript automatically infers type predicates for simple guard functions:
const isNumber = (x: unknown) => typeof x === 'number';
// Inferred: (x: unknown) => x is number
const isNonNullish = <T>(x: T) => x != null;
// Inferred: <T>(x: T) => x is NonNullable<T>
// Array.filter benefits from inferred predicates
const nums = [1, 2, null, 3].filter((x) => x !== null);
// nums: number[] (not (number | null)[])
const birds = countries
.map((country) => nationalBirds.get(country))
.filter((bird) => bird !== undefined);
// birds: Bird[] (not (Bird | undefined)[])The function must return a boolean, take a single parameter, and not use explicit return type annotation. Add an explicit x is Type annotation if the inference is not what you want.
Narrowing with Destructuring
Narrowing can be lost with destructuring:
type Response = { ok: true; data: string } | { ok: false; error: string };
function handleResponse(response: Response) {
const { ok } = response;
if (ok) {
console.log(response.data);
}
}
function handleResponseCorrect(response: Response) {
if (response.ok) {
console.log(response.data);
}
}Keep the object intact for discriminated union narrowing to work.
Type Utilities
TypeScript provides built-in utility types for common type transformations. These are globally available without imports.
Pick and Omit
Extract or exclude specific properties from a type:
type User = {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
};
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
type UserWithoutPassword = Omit<User, 'password'>;
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;Pick is useful for selecting a subset of properties. Omit is better when you want most properties except a few.
Partial and Required
Make all properties optional or required:
type UpdateUserInput = Partial<User>;
function updateUser(id: string, updates: UpdateUserInput) {
// All fields are optional, but type-safe
}
type Config = {
host?: string;
port?: number;
ssl?: boolean;
};
type RequiredConfig = Required<Config>;Partial is commonly used for update operations where you only change some fields.
Record
Create an object type with known keys:
type Role = 'admin' | 'editor' | 'viewer';
type Permissions = Record<Role, string[]>;
const permissions: Permissions = {
admin: ['read', 'write', 'delete'],
editor: ['read', 'write'],
viewer: ['read'],
};
type ErrorMessages = Record<string, string>;
const errors: ErrorMessages = {
notFound: 'Resource not found',
unauthorized: 'Access denied',
};Record<K, V> is equivalent to { [key in K]: V } but more concise.
Extract and Exclude
Filter union types:
type Status = 'pending' | 'approved' | 'rejected' | 'cancelled';
type ActiveStatus = Exclude<Status, 'cancelled'>;
type EndStatus = Extract<Status, 'approved' | 'rejected'>;
type Primitive = string | number | boolean | null | undefined;
type NonNullable<T> = Exclude<T, null | undefined>;Exclude removes types from a union. Extract keeps only matching types.
ReturnType and Parameters
Extract types from functions:
function createUser(name: string, email: string): User {
return { id: crypto.randomUUID(), name, email, createdAt: new Date() };
}
type CreateUserReturn = ReturnType<typeof createUser>;
type CreateUserParams = Parameters<typeof createUser>;
type FirstParam = Parameters<typeof createUser>[0];
async function fetchData(): Promise<{ items: string[] }> {
return { items: [] };
}
type FetchDataReturn = ReturnType<typeof fetchData>;
type UnwrappedReturn = Awaited<FetchDataReturn>;Prefer typing the function return directly rather than extracting it with ReturnType.
Awaited
Unwrap Promise types recursively:
type PromiseValue = Awaited<Promise<string>>;
type NestedPromise = Awaited<Promise<Promise<number>>>;
async function getData(): Promise<{ user: User }> {
return { user: { id: '1', name: 'Alice' } };
}
type Data = Awaited<ReturnType<typeof getData>>;
type MaybePromise<T> = T | Promise<T>;
type Resolved<T> = T extends Promise<infer U> ? U : T;
const value: Resolved<MaybePromise<string>> = 'hello';Awaited is particularly useful with async functions and Promise-based APIs.
Readonly and Immutability
Make properties readonly:
type ImmutableUser = Readonly<User>;
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
const config: Readonly<{ port: number }> = { port: 3000 };Built-in Readonly only applies to top-level properties. Use DeepReadonly for nested objects.
NonNullable
Remove null and undefined from a type:
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
function processUser(user: User | null) {
if (user !== null) {
const definite: NonNullable<typeof user> = user;
}
}NonNullable is equivalent to Exclude<T, null | undefined>.
Combining Utilities
Chain utilities for complex transformations:
type PartialUpdateUser = Partial<Omit<User, 'id' | 'createdAt'>>;
type RequiredPublicUser = Required<Pick<User, 'id' | 'name'>>;
type ReadonlyCreateInput = Readonly<Omit<User, 'id'>>;
type UserKeys = keyof User;
type OptionalUserKeys = {
[K in keyof User]?: User[K];
};
type StringKeys<T> = Extract<keyof T, string>;
type UserStringKeys = StringKeys<User>;Combine utilities to express complex type relationships without creating intermediate types.
ConstructorParameters and InstanceType
Extract types from class constructors:
class Database {
constructor(
public host: string,
public port: number,
) {}
}
type DbParams = ConstructorParameters<typeof Database>;
type DbInstance = InstanceType<typeof Database>;
function createDb(...args: ConstructorParameters<typeof Database>) {
return new Database(...args);
}These are less commonly used but useful for factory patterns and dependency injection.
NoInfer
Prevent TypeScript from inferring a type parameter from a specific position:
function createStreetLight<C extends string>(
colors: C[],
defaultColor?: NoInfer<C>,
) {
// ...
}
createStreetLight(['red', 'yellow', 'green'], 'red'); // OK
createStreetLight(['red', 'yellow', 'green'], 'blue'); // Error
function createConfig<T extends string>(options: T[], initial: NoInfer<T>) {
return { options, initial };
}NoInfer<T> forces the type to be inferred from other positions (e.g., the first argument), preventing widening from the marked position.
ThisParameterType and OmitThisParameter
Work with function this types:
function greet(this: User, message: string) {
return `${this.name} says: ${message}`;
}
type GreetThis = ThisParameterType<typeof greet>;
type GreetWithoutThis = OmitThisParameter<typeof greet>;
const boundGreet: GreetWithoutThis = greet.bind(user);Rarely needed in modern TypeScript but useful for working with function contexts.