TypeScript Advanced Types: Mapped Types, Conditionals, and More
TypeScript Advanced Types: Mapped Types, Conditionals, and More
TypeScript's type system is incredibly powerful. Let's explore advanced features that will help you write more robust and expressive code.
Mapped Types
Mapped types allow you to create new types based on existing ones.
Basic Mapped Types
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Make all properties mutable
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
interface User {
readonly id: number;
name: string;
age?: number;
}
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
type MutableUser = Mutable<User>;
Custom Mapped Types
// Get the type of a specific property
type PropertyType<T, K extends keyof T> = T[K];
// Create a type with only string properties
type StringProperties<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Config {
host: string;
port: number;
debug: boolean;
apiKey: string;
}
type StringConfig = StringProperties<Config>;
// { host: string; apiKey: string; }
// Rename properties
type GetterNames<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = GetterNames<User>;
// { getName: () => string; getAge: () => number | undefined; ... }
Conditional Types
Conditional types select types based on conditions.
Basic Conditional Types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Check for array
type IsArray<T> = T extends any[] ? true : false;
type C = IsArray<string[]>; // true
type D = IsArray<string>; // false
infer Keyword
The infer keyword allows you to extract types:
// Extract return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(): string { return 'hello'; }
type GreetReturn = ReturnType<typeof greet>; // string
// Extract function parameters
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function add(a: number, b: string): void {}
type AddParams = Parameters<typeof add>; // [number, string]
// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;
type E = ElementType<string[]>; // string
type F = ElementType<number[]>; // number
// Extract Promise value
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type G = Awaited<Promise<Promise<string>>>; // string
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
type StrArr = ToArray<string>; // string[]
type NumArr = ToArray<number>; // number[]
// When T is a union, it distributes
type StrOrNumArr = ToArray<string | number>; // string[] | number[]
// Non-distributive with tuple
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type StrOrNumArr2 = ToArrayNonDist<string | number>; // (string | number)[]
Template Literal Types
Template literal types manipulate string types:
// Basic template literal
type Greeting = `hello ${string}`;
const a: Greeting = 'hello world'; // OK
const b: Greeting = 'hi there'; // Error!
// String manipulation
type UppercaseName = Uppercase<'hello'>; // 'HELLO'
type LowercaseName = Lowercase<'HELLO'>; // 'hello'
type CapitalizeName = Capitalize<'hello'>; // 'Hello'
type UncapitalizeName = Uncapitalize<'Hello'>; // 'hello'
// Combine with mapped types
type EventName<T extends string> = `on${Capitalize<T>}`;
type MouseEvents = EventName<'click' | 'mousedown' | 'mouseup'>;
// 'onClick' | 'onMousedown' | 'onMouseup'
// Get all possible routes
type Routes = '/home' | '/about' | '/contact';
type ApiRoutes = `/api${Routes}`;
// '/api/home' | '/api/about' | '/api/contact'
Advanced Template Literals
// Extract part of a string
type ExtractPath<S extends string> = S extends `/${infer Path}` ? Path : never;
type P = ExtractPath<'/users/123'>; // 'users/123'
// Parse route parameters
type RouteParams<Route extends string> =
Route extends `${string}:${infer Param}/${infer Rest}`
? Param | RouteParams<`/${Rest}`>
: Route extends `${string}:${infer Param}`
? Param
: never;
type Params = RouteParams<'/users/:id/posts/:postId'>;
// 'id' | 'postId'
Utility Types
Built-in Utilities
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }
// Omit specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// { id: number; name: string; age: number; }
// Extract from union
type T = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
// Exclude from union
type U = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c'
// Non-nullable
type V = NonNullable<string | null | undefined>; // string
// Record type
type UserRoles = Record<'admin' | 'user' | 'guest', { permissions: string[] }>;
Custom Utility Types
// Deep partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
interface NestedConfig {
server: {
host: string;
port: number;
};
database: {
url: string;
pool: {
min: number;
max: number;
};
};
}
type PartialConfig = DeepPartial<NestedConfig>;
// Deep required
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
// Merge types
type Merge<A, B> = Omit<A, keyof B> & B;
type Base = { id: number; name: string };
type Extra = { name: string; email: string };
type Merged = Merge<Base, Extra>;
// { id: number; name: string; email: string }
// Make specific keys required
type RequireKeys<T, K extends keyof T> = T & Required<Pick<T, K>>;
interface OptionalUser {
id?: number;
name?: string;
email?: string;
}
type UserWithId = RequireKeys<OptionalUser, 'id'>;
// { id: number; name?: string; email?: string; }
Type Guards and Narrowing
Type Guards
// typeof guard
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // string
}
return value * 2; // number
}
// instanceof guard
class Dog { bark() {} }
class Cat { meow() {} }
function makeSound(animal: Dog | Cat) {
if (animal instanceof Dog) {
animal.bark();
} else {
animal.meow();
}
}
// in operator guard
interface Bird { fly(): void; }
interface Fish { swim(): void; }
function move(animal: Bird | Fish) {
if ('fly' in animal) {
animal.fly();
} else {
animal.swim();
}
}
Custom Type Guards
interface User {
id: number;
name: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value &&
typeof (value as User).id === 'number' &&
typeof (value as User).name === 'string'
);
}
function processValue(value: unknown) {
if (isUser(value)) {
console.log(value.name); // TypeScript knows it's User
}
}
// Asserts functions
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error('Value is null or undefined');
}
}
function processString(value: string | null) {
assertDefined(value);
console.log(value.toUpperCase()); // value is string
}
Practical Examples
API Response Types
type ApiResponse<T> = {
data: T;
status: 'success' | 'error';
message: string;
timestamp: number;
};
type PaginatedResponse<T> = ApiResponse<T[]> & {
pagination: {
page: number;
limit: number;
total: number;
};
};
// Type-safe fetch
async function fetchApi<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
return response.json();
}
const users = await fetchApi<User[]>('/api/users');
Type-safe Event Emitter
type EventMap = {
click: { x: number; y: number };
focus: { element: HTMLElement };
change: { value: string };
};
class TypedEventEmitter<T extends Record<string, any>> {
private listeners = new Map<keyof T, Set<Function>>();
on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
}
emit<K extends keyof T>(event: K, data: T[K]) {
this.listeners.get(event)?.forEach(listener => listener(data));
}
}
const emitter = new TypedEventEmitter<EventMap>();
emitter.on('click', (data) => console.log(data.x)); // Type-safe!
emitter.emit('click', { x: 100, y: 200 });
Conclusion
TypeScript's advanced type system provides powerful tools for creating robust, type-safe applications. Mapped types, conditional types, and template literal types enable you to build complex type relationships while maintaining type safety. Practice these patterns to write more expressive and safer code!