Introduction
TypeScript's type system is incredibly powerful and flexible. Understanding advanced type patterns enables you to create type-safe, maintainable code that catches errors at compile time.
Conditional Types
Basic Conditional Types
typescript
// Syntax: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<'hello'>; // trueGeneric Conditional Types
typescript
type NonNullable<T> = T extends null | undefined ? never : T;
type Value1 = NonNullable<string | null>; // string
type Value2 = NonNullable<number | undefined>; // number
type Value3 = NonNullable<null>; // never
// Extract promise inner type
type Unpromise<T> = T extends Promise<infer U> ? U : T;
type Data = Unpromise<Promise<number>>; // number
type Text = Unpromise<string>; // stringDistributive Conditional Types
typescript
// Distributes over union types
type ToArray<T> = T extends any ? T[] : never;
type Numbers = ToArray<string | number>; // string[] | number[]
// Non-distributive
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;
type Numbers2 = ToArrayNonDistributive<string | number>; // (string | number)[]Mapped Types
Basic Mapped Types
typescript
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Optional<T> = {
[P in keyof T]?: T[P];
};
interface User {
id: number;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
type PartialUser = Optional<User>;Key Remapping with 'as'
typescript
type Getters<T> = {
[P in keyof T as `get${Capitalize<P & string>} `]: () => T[P];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// }
// Filter keys
type OnlyStrings<T> = {
[P in keyof T as T[P] extends string ? P : never]: T[P];
};
type StringProps = OnlyStrings<User>;
// {
// name: string;
// }Template Literal Types
typescript
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type ChangeEvent = EventName<'change'>; // 'onChange'
// Combine template literals
type AllEvents = EventName<'click' | 'change' | 'submit'>;
// 'onClick' | 'onChange' | 'onSubmit'
// Complex combinations
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';
type ColorSize = `${Color}-${Size}`;
// 'red-small' | 'red-medium' | ... | 'green-large'
// String manipulation
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;
type TrimRight<T extends string> = T extends `${infer Rest} ` ? TrimRight<Rest> : T;
type Trim<T extends string> = TrimLeft<TrimRight<T>>;
type Test1 = Trim<' hello '>; // 'hello'Advanced Utility Types
Deep Partial
typescript
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
api: {
timeout: number;
};
}
type PartialConfig = DeepPartial<Config>;
const config: PartialConfig = {
database: {
host: 'localhost'
// port, credentials optional
}
};Deep Required
typescript
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object
? DeepRequired<T[P]>
: T[P];
};
interface PartialData {
user?: {
name?: string;
age?: number;
};
}
type CompleteData = DeepRequired<PartialData>;
// {
// user: {
// name: string;
// age: number;
// };
// }Deep Readonly
typescript
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface State {
user: {
name: string;
preferences: {
theme: string;
};
};
}
type ReadonlyState = DeepReadonly<State>;
const state: ReadonlyState = {
user: {
name: 'John',
preferences: {
theme: 'dark'
}
}
};
// state.user.name = 'Jane'; // Error
// state.user.preferences.theme = 'light'; // ErrorMutable
typescript
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
interface ReadonlyInterface {
readonly id: number;
readonly name: string;
}
type MutableInterface = Mutable<ReadonlyInterface>;
// {
// id: number;
// name: string;
// }Type Manipulation
Pick and Omit
typescript
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Pick specific properties
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// Omit specific properties
type SafeUser = Omit<User, 'password'>;
// Combined
type UserDTO = Pick<PublicUser, 'id' | 'name'>;Extract and Exclude
typescript
type Primitives = string | number | boolean | null | undefined;
// Exclude types from union
type NonNullable = Exclude<Primitives, null | undefined>;
// string | number | boolean
// Extract types from union
type OnlyStrings = Extract<Primitives, string>;
// string
type Functions = Extract<string | number | (() => void), Function>;
// () => voidReturnType and Parameters
typescript
function getUser(id: number): { id: number; name: string } {
return { id, name: 'John' };
}
type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string }
async function fetchData(url: string): Promise<number> {
return 42;
}
type DataReturn = ReturnType<typeof fetchData>;
// Promise<number>
type DataAwaited = Awaited<DataReturn>;
// number
// Extract function parameters
type Params = Parameters<typeof getUser>;
// [number]Branded Types
Creating Branded Types
typescript
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };
const usd = (amount: number): USD => amount as USD;
const eur = (amount: number): EUR => amount as EUR;
const addUSD = (a: USD, b: USD): USD => {
return usd((a + b) as number);
};
// Type-safe - can't mix currencies
const total1 = addUSD(usd(100), usd(50)); // ✓ Valid
const total2 = addUSD(usd(100), eur(50)); // ✗ Error
// Use branded types in APIs
function transferUSD(amount: USD, toAccount: string) {
// Process transfer
}
transferUSD(usd(500), 'account-123'); // ✓ Valid
transferUSD(500, 'account-123'); // ✗ ErrorEmail Branded Type
typescript
type Email = string & { readonly __brand: 'Email' };
const email = (value: string): Email => {
if (!value.includes('@')) {
throw new Error('Invalid email');
}
return value as Email;
};
function sendEmail(to: Email, subject: string, body: string) {
// Send email
}
sendEmail(email('user@example.com'), 'Hello', 'World'); // ✓ Valid
sendEmail('invalid-email', 'Hello', 'World'); // ✗ ErrorType Guards and Discriminated Unions
Type Guards
typescript
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isNumber(value: unknown): value is number {
return typeof value === 'number';
}
function process(value: unknown) {
if (isString(value)) {
return value.toUpperCase(); // TypeScript knows it's string
}
if (isNumber(value)) {
return value * 2; // TypeScript knows it's number
}
return null;
}Discriminated Unions
typescript
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number }
| { kind: 'triangle'; base: number; height: number };
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 0.5 * shape.base * shape.height;
}
}
// Exhaustive checking
function areaExhaustive(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return 0.5 * shape.base * shape.height;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}Advanced Pattern Matching
typescript
type Result<T, E> =
| { success: true; data: T }
| { success: false; error: E };
function handleResult<T, E>(result: Result<T, E>) {
if (result.success) {
console.log(result.data); // T
} else {
console.log(result.error); // E
}
}
// Generic pattern matcher
type Match<T, Cases> = Cases extends {
[K in keyof Cases]: (value: Extract<T, { _type: K }>) => any
}
? { [K in keyof Cases]: ReturnType<Cases[K]> }[keyof Cases]
: never;
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: { data: string } }
| { type: 'FETCH_ERROR'; error: string };
function matchAction<T>(action: Action, cases: T): Match<Action, T> {
const caseKey = action.type as keyof T;
const handler = cases[caseKey] as (action: Action) => any;
return handler(action);
}
const result = matchAction(
{ type: 'FETCH_SUCCESS', payload: { data: 'hello' } },
{
FETCH_START: () => 'Loading...',
FETCH_SUCCESS: (action) => action.payload.data,
FETCH_ERROR: (action) => action.error
}
);Recursive Types
typescript
// JSON type
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
const data: JSONValue = {
name: 'John',
age: 30,
isActive: true,
tags: ['developer', 'typescript'],
metadata: {
created: '2025-01-01',
updated: null
}
};
// Tree structure
type TreeNode<T> = {
value: T;
children: TreeNode<T>[];
};
const tree: TreeNode<string> = {
value: 'root',
children: [
{
value: 'child1',
children: []
},
{
value: 'child2',
children: [
{
value: 'grandchild',
children: []
}
]
}
]
};Type-Level Programming
typescript
// Type-level arithmetic
type Add<A extends number, B extends number> =
[...Tuple<A>, ...Tuple<B>]['length'] extends number
? [...Tuple<A>, ...Tuple<B>]['length']
: never;
type Tuple<T extends number, U extends any[] = []> =
U['length'] extends T ? U : Tuple<T, [...U, any]>;
type Result = Add<2, 3>; // 5
// Type-level string manipulation
type Split<S extends string, D extends string> =
S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];
type Parts = Split<'hello-world-foo', '-'>;
// ['hello', 'world', 'foo']
// Type-level object path
type Path<T, K extends string> =
K extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? Path<T[Key], Rest>
: never
: K extends keyof T
? T[K]
: never;
interface Config {
database: {
connection: {
host: string;
port: number;
};
};
}
type Host = Path<Config, 'database.connection.host'>; // stringBest Practices
1. Prefer Type Inference
typescript
// Let TypeScript infer when possible
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000
}; // Type inferred
// Only specify types when necessary
interface User {
id: number;
name: string;
}2. Use Utility Types
typescript
// Leverage built-in utility types
type Update<T> = Partial<T>;
type Readonly = Readonly<T>;
type Pick = Pick<T, K>;3. Create Reusable Types
typescript
// Generic, reusable type definitions
type ApiResponse<T> = {
data: T;
status: number;
message: string;
};
type PaginatedResponse<T> = ApiResponse<{
items: T[];
total: number;
page: number;
}>;4. Document Complex Types
typescript
/**
* Deep partial type - makes all nested properties optional
* @example
* type Config = DeepPartial<{
* database: { host: string; port: number };
* }>;
* // Can specify: { database: { host: 'localhost' } }
*/
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};Real-World Example: Form Builder
typescript
type FieldType = 'text' | 'number' | 'email' | 'password' | 'select';
type Field<T extends FieldType> = T extends 'select'
? {
type: T;
name: string;
label: string;
options: string[];
required?: boolean;
}
: {
type: T;
name: string;
label: string;
placeholder?: string;
required?: boolean;
};
type FormSchema<T extends readonly Field<any>[]> = {
[K in keyof T]: T[K] extends Field<infer U>
? U extends 'select'
? { [Name in T[K]['name']]: string }
: U extends 'number'
? { [Name in T[K]['name']]: number }
: { [Name in T[K]['name']]: string }
: never
}[number];
const schema = [
{ type: 'text' as const, name: 'username', label: 'Username' },
{ type: 'email' as const, name: 'email', label: 'Email' },
{ type: 'select' as const, name: 'role', label: 'Role', options: ['admin', 'user'] }
] as const;
type FormData = FormSchema<typeof schema>;
// {
// username: string;
// email: string;
// role: string;
// }Conclusion
Advanced TypeScript types provide powerful tools for creating type-safe applications. Master these patterns to write more robust and maintainable code.
Key takeaways:
- Conditional types enable flexible type logic
- Mapped types transform properties systematically
- Template literal types manipulate strings at type level
- Utility types simplify common type operations
- Branded types add runtime safety to primitives
- Type guards narrow types correctly
Use these advanced type patterns to catch errors at compile time and improve your code quality!