TypeScript Advanced Types: A Deep Dive
TypeScript’s type system is powerful. After using advanced types in production, here’s how to leverage them effectively.
Conditional Types
Basic Conditional Types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Extract and Exclude
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type T1 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type T2 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c'
Function Return Type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getString(): string {
return 'hello';
}
type R = ReturnType<typeof getString>; // string
Mapped Types
Basic Mapped Type
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Required<T> = {
[P in keyof T]-?: T[P];
};
Key Remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type User = {
name: string;
age: number;
};
type UserGetters = Getters<User>;
// {
// getName: () => string;
// getAge: () => number;
// }
Template Literal Types
String Manipulation
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
type SubmitEvent = EventName<'submit'>; // 'onSubmit'
Path Types
type Path<T> = T extends object
? {
[K in keyof T]: K extends string
? K | `${K}.${Path<T[K]>}`
: never;
}[keyof T]
: never;
type User = {
name: string;
address: {
street: string;
city: string;
};
};
type UserPath = Path<User>;
// 'name' | 'address' | 'address.street' | 'address.city'
Utility Types
Pick and Omit
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
type User = {
id: string;
name: string;
email: string;
password: string;
};
type PublicUser = Omit<User, 'password'>;
// { id: string; name: string; email: string; }
Record
type Record<K extends keyof any, T> = {
[P in K]: T;
};
type UserRoles = Record<string, boolean>;
// { [key: string]: boolean; }
Type Guards
Custom Type Guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function process(value: unknown) {
if (isString(value)) {
// value is string here
console.log(value.toUpperCase());
}
}
Discriminated Unions
type Success<T> = {
type: 'success';
data: T;
};
type Error = {
type: 'error';
message: string;
};
type Result<T> = Success<T> | Error;
function handleResult<T>(result: Result<T>) {
if (result.type === 'success') {
// result is Success<T>
console.log(result.data);
} else {
// result is Error
console.error(result.message);
}
}
Branded Types
Type Branding
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type ProductId = Brand<string, 'ProductId'>;
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId) {
// Implementation
}
const userId = createUserId('123');
getUser(userId); // OK
getUser('123'); // Error: Type 'string' is not assignable to type 'UserId'
Best Practices
- Use utility types - Built-in helpers
- Create reusable types - DRY principle
- Type guards - Narrow types safely
- Document complex types - Clear comments
- Test types - Type-level tests
- Avoid over-engineering - Keep it simple
- Use generics - Reusable types
- Leverage inference - Let TypeScript infer
Conclusion
Advanced TypeScript types enable:
- Type safety
- Better DX
- Self-documenting code
- Compile-time checks
Start with utility types, then explore conditional and mapped types. The patterns shown here work for production code.
TypeScript advanced types from May 2021, covering conditional types, mapped types, and utility types.