| name | typescript-advanced |
| description | Advanced TypeScript type system patterns including generics, conditional types, mapped types, template literals, branded types, and utility types. Use when implementing complex type systems, creating reusable type utilities, building type-safe APIs, or ensuring compile-time type safety. |
TypeScript Advanced Types
Comprehensive guidance for TypeScript's advanced type system.
When to Use This Skill
- Building type-safe libraries or APIs
- Creating reusable generic components
- Implementing complex type inference logic
- Designing type-safe state management
- Migrating JavaScript to TypeScript
- Working with complex domain models
- Creating compile-time validation
Core Patterns
1. Generics
Basic Generic Function:
function identity<T>(value: T): T {
return value;
}
const num = identity(42);
const str = identity("hello");
Generic Constraints:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(item.length);
return item;
}
logLength("hello");
logLength([1, 2, 3]);
Multiple Type Parameters:
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const merged = merge({ name: "John" }, { age: 30 });
2. Conditional Types
Basic Conditional Type:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>;
type B = IsString<number>;
Extracting Types with infer:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type ElementType<T> = T extends (infer E)[] ? E : never;
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
function getUser() {
return { id: 1, name: "John" };
}
type User = ReturnType<typeof getUser>;
Distributive Conditional Types:
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
3. Mapped Types
Basic Mapped Types:
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Key Remapping (TypeScript 4.1+):
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
Filtering Properties:
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
age: number;
active: boolean;
}
type OnlyNumbers = PickByType<Mixed, number>;
4. Template Literal Types
Basic Template Literals:
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Route = `/api/${'users' | 'posts'}`;
type Endpoint = `${HTTPMethod} ${Route}`;
Nested Path Types:
type PathKeys<T> = T extends object
? {
[K in keyof T & string]: K | `${K}.${PathKeys<T[K]>}`;
}[keyof T & string]
: never;
interface Config {
server: {
host: string;
port: number;
};
database: {
url: string;
};
}
type ConfigPath = PathKeys<Config>;
5. Branded Types
Prevent mixing similar primitives:
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId) { }
const userId = createUserId('user_123');
const productId = 'prod_456' as ProductId;
getUser(userId);
Branded types for validation:
type ValidEmail = string & { readonly __brand: 'ValidEmail' };
function validateEmail(email: string): ValidEmail | null {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email) ? (email as ValidEmail) : null;
}
function sendEmail(to: ValidEmail, subject: string) {
}
6. Discriminated Unions
Type-safe state machines:
type LoadingState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string[] }
| { status: 'error'; error: Error };
function renderState(state: LoadingState): string {
switch (state.status) {
case 'idle':
return 'Not started';
case 'loading':
return 'Loading...';
case 'success':
return `Loaded ${state.data.length} items`;
case 'error':
return `Error: ${state.error.message}`;
}
}
Redux-style actions:
type Action =
| { type: 'FETCH_USER'; payload: { userId: string } }
| { type: 'UPDATE_USER'; payload: { userId: string; data: Partial<User> } }
| { type: 'DELETE_USER'; payload: { userId: string } }
| { type: 'CLEAR_USERS' };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_USER':
return { ...state, loading: true };
case 'UPDATE_USER':
return updateUser(state, action.payload);
case 'DELETE_USER':
return deleteUser(state, action.payload.userId);
case 'CLEAR_USERS':
return { ...state, users: [] };
}
}
7. Type Guards
Basic type guards:
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isArrayOf<T>(
value: unknown,
guard: (item: unknown) => item is T
): value is T[] {
return Array.isArray(value) && value.every(guard);
}
const data: unknown = ['a', 'b', 'c'];
if (isArrayOf(data, isString)) {
data.forEach(s => s.toUpperCase());
}
Assertion functions:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Not a string');
}
}
function processValue(value: unknown) {
assertIsString(value);
console.log(value.toUpperCase());
}
Advanced Patterns
Deep Utility Types
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? T[P] extends Function
? T[P]
: DeepPartial<T[P]>
: T[P];
};
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? T[P] extends Function
? T[P]
: DeepReadonly<T[P]>
: T[P];
};
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object
? DeepRequired<T[P]>
: T[P];
};
Type-Safe Event Emitter
type EventMap = {
'user:created': { id: string; name: string };
'user:deleted': { id: string };
};
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: {
[K in keyof T]?: Array<(data: T[K]) => void>;
} = {};
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.listeners[event]?.forEach(cb => cb(data));
}
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on('user:created', (data) => {
console.log(data.id, data.name);
});
emitter.emit('user:created', { id: '1', name: 'John' });
Builder Pattern with Type State
class QueryBuilder<T, HasSelect extends boolean = false> {
private selectFields?: string[];
private whereClause?: string;
select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>, true> {
this.selectFields = fields as string[];
return this as any;
}
where(clause: string): this {
this.whereClause = clause;
return this;
}
execute(this: QueryBuilder<T, true>): Promise<T[]> {
return Promise.resolve([]);
}
}
interface User {
id: number;
name: string;
email: string;
}
const query = new QueryBuilder<User>();
query.select('id', 'name').execute();
Function Type Utilities
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type Curry<T extends any[], R> = T extends [infer First, ...infer Rest]
? (arg: First) => Curry<Rest, R>
: R;
type Add = Curry<[number, number], number>;
Best Practices
- Use
unknown over any - Enforce type checking
- Prefer
interface for objects - Better error messages, extensible
- Use
type for unions/intersections - More flexible for complex types
- Leverage inference - Don't over-annotate; let TypeScript infer
- Use const assertions -
as const preserves literal types
- Avoid type assertions - Use type guards instead of
as
- Document complex types - Add JSDoc comments
- Test your types - Use type-level tests
Common Pitfalls
- Over-using
any - Defeats TypeScript's purpose
- Ignoring strict null checks - Can lead to runtime errors
- Too complex types - Can slow compilation
- Not using discriminated unions - Misses type narrowing opportunities
- Circular type references - Can cause compiler errors
- Forgetting readonly - Allows unintended mutations
Type Testing
type AssertEqual<T, U> =
[T] extends [U]
? [U] extends [T]
? true
: false
: false;
type Test1 = AssertEqual<string, string>;
type Test2 = AssertEqual<string, number>;
type Expect<T extends true> = T;
type TestGetters = Expect<AssertEqual<
Getters<{ name: string }>,
{ getName: () => string }
>>;
Resources