Advanced TypeScript techniques used by senior engineers — discriminated unions, satisfies, template literal types, and more.
Instead of boolean flags and optional fields, use discriminated unions to make impossible states unrepresentable:
// Bad — what is state when isLoading=false and error is undefined?
interface State {
isLoading: boolean;
data?: User;
error?: Error;
}
// Good — every state is explicit
type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: Error };
// TypeScript narrows correctly:
if (state.status === 'success') {
console.log(state.data.name); // data is guaranteed here
}
satisfies OperatorAdded in TypeScript 4.9, satisfies validates a value against a type while preserving the most specific literal type:
type Color = 'red' | 'green' | 'blue';
type Config = Record<string, Color>;
// Without satisfies — loses specific keys
const theme: Config = { primary: 'red', secondary: 'blue' };
// theme.primary is typed as Color, not 'red'
// With satisfies — validates AND preserves literals
const theme = {
primary: 'red',
secondary: 'blue',
} satisfies Config;
// theme.primary is typed as 'red' (more specific)
Build precise string types from combinations:
type Direction = 'top' | 'bottom' | 'left' | 'right';
type Spacing = 'sm' | 'md' | 'lg';
type SpacingClass = `p${Capitalize<Direction>}-${Spacing}`;
// 'pTop-sm' | 'pTop-md' | 'pBottom-sm' | ...
// Event handler naming
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
// API endpoint typing
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = `/api/${string}`;
Extract types from complex shapes:
// Get the return type of a Promise
type Awaited<T> = T extends Promise<infer R> ? R : T;
// Get the element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
// Get the first argument type of a function
type FirstArg<T extends (...args: any) => any> =
T extends (first: infer A, ...rest: any) => any ? A : never;
type UserId = FirstArg<typeof getUser>; // string
// Make all properties optional
type Partial<T> = { [K in keyof T]?: T[K] };
// Make all properties required (remove ?)
type Required<T> = { [K in keyof T]-?: T[K] };
// Make all properties readonly
type Readonly<T> = { readonly [K in keyof T]: T[K] };
// Deep partial (recursive)
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
as const// Without as const — type is string[]
const colors = ['red', 'green', 'blue'];
// With as const — type is readonly ['red', 'green', 'blue']
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number]; // 'red' | 'green' | 'blue'
// Object as const
const ROUTES = {
home: '/',
tools: '/tools',
blog: '/blog',
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES]; // '/' | '/tools' | '/blog'
// Different input types → different output types
function parseId(id: string): number;
function parseId(id: number): string;
function parseId(id: string | number): string | number {
return typeof id === 'string' ? parseInt(id) : id.toString();
}
const num = parseId('123'); // typed as number
const str = parseId(123); // typed as string
// Constraint — T must have a name property
function getName<T extends { name: string }>(item: T): string {
return item.name;
}
// Default generic type
interface Repository<T = User> {
findById(id: string): Promise<T>;
save(item: T): Promise<void>;
}
// Usage without specifying generic (uses User default)
const repo: Repository = createRepo();
Prevent mixing up values of the same primitive type:
type UserId = string & { readonly _brand: 'UserId' };
type ProductId = string & { readonly _brand: 'ProductId' };
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId): User { /* ... */ }
const userId = createUserId('u_123');
const productId = 'p_456' as ProductId;
getUser(userId); // ✓ OK
getUser(productId); // ✗ Type error — ProductId ≠ UserId
getUser('raw-string'); // ✗ Type error
When working with API responses, instead of writing types manually, paste the JSON response into HeoLab's JSON → TypeScript converter to get accurate interfaces instantly. Then refine with the patterns above.
// Generated from API response JSON:
interface ApiResponse {
user: {
id: string;
name: string;
email: string;
roles: string[];
};
token: string;
}
These patterns separate good TypeScript from great TypeScript. Start with discriminated unions and as const — they have the highest impact to effort ratio. Add satisfies for configuration objects and template literal types when you need precise string contracts.