TypeScript
TypeScript Advanced Types: From Utility Types to Template Literals
15 min
TypeScriptTypesGenericsUtility TypesType Safety
Deep dive into TypeScript's type system with practical examples of utility types, conditional types, mapped types, and template literal types. Master type-level programming.
TypeScript's type system is incredibly powerful, going far beyond basic type annotations. In this guide, we'll explore advanced type features that enable type-safe, maintainable codebases at scale.
Utility Types: Your Type Toolkit
TypeScript provides built-in utility types that transform existing types. These are essential for real-world applications.
1// Partial<T> - Makes all properties optional
2interface User {
3 id: number;
4 name: string;
5 email: string;
6 role: 'admin' | 'user';
7}
8
9// Update function accepts partial user data
10function updateUser(id: number, updates: Partial<User>) {
11 // Implementation
12}
13
14updateUser(1, { email: 'new@email.com' }); // ✅ Only email needed
15
16// Required<T> - Makes all properties required
17type UserWithDefaults = Partial<User>;
18type CompleteUser = Required<UserWithDefaults>; // All fields required again
19
20// Readonly<T> - Makes all properties immutable
21const config: Readonly<User> = {
22 id: 1,
23 name: 'Admin',
24 email: 'admin@app.com',
25 role: 'admin'
26};
27// config.email = 'test'; // ❌ Error: Cannot assign to 'email'
28
29// Pick<T, K> - Select specific properties
30type UserPublicInfo = Pick<User, 'name' | 'email'>;
31// { name: string; email: string; }
32
33// Omit<T, K> - Remove specific properties
34type UserWithoutId = Omit<User, 'id'>;
35// { name: string; email: string; role: 'admin' | 'user'; }
36
37// Record<K, T> - Create object type with specific keys
38type UserRoles = Record<'admin' | 'user' | 'guest', string[]>;
39const permissions: UserRoles = {
40 admin: ['read', 'write', 'delete'],
41 user: ['read', 'write'],
42 guest: ['read']
43};
Conditional Types: Type-Level if Statements
Conditional types enable types that change based on conditions. They unlock powerful type inference patterns.
1// Basic conditional type syntax: T extends U ? X : Y
2type IsString<T> = T extends string ? true : false;
3
4type A = IsString<string>; // true
5type B = IsString<number>; // false
6
7// Real-world example: Type-safe API response handler
8type ApiResponse<T> = T extends { error: any }
9 ? { success: false; error: string }
10 : { success: true; data: T };
11
12function handleResponse<T>(response: ApiResponse<T>) {
13 if (response.success) {
14 // TypeScript knows response.data exists here
15 console.log(response.data);
16 } else {
17 // TypeScript knows response.error exists here
18 console.error(response.error);
19 }
20}
21
22// Exclude<T, U> - Remove types from union
23type AllTypes = 'click' | 'focus' | 'blur' | 'submit';
24type ClickTypes = Exclude<AllTypes, 'focus' | 'blur'>; // 'click' | 'submit'
25
26// Extract<T, U> - Extract types from union
27type EventNames = Extract<AllTypes, 'click' | 'submit'>; // 'click' | 'submit'
28
29// NonNullable<T> - Remove null and undefined
30type MaybeString = string | null | undefined;
31type DefinitelyString = NonNullable<MaybeString>; // string
32
33// Advanced: Recursive conditional types
34type DeepReadonly<T> = {
35 readonly [P in keyof T]: T[P] extends object
36 ? DeepReadonly<T[P]>
37 : T[P];
38};
39
40interface NestedConfig {
41 database: {
42 host: string;
43 credentials: {
44 username: string;
45 password: string;
46 };
47 };
48}
49
50type ImmutableConfig = DeepReadonly<NestedConfig>;
51// All nested properties are readonly
Mapped Types: Transform Existing Types
1// Basic mapped type
2type Nullable<T> = {
3 [P in keyof T]: T[P] | null;
4};
5
6interface Post {
7 id: number;
8 title: string;
9 body: string;
10}
11
12type NullablePost = Nullable<Post>;
13// { id: number | null; title: string | null; body: string | null; }
14
15// With modifiers: readonly and optional
16type ReadonlyNullable<T> = {
17 readonly [P in keyof T]?: T[P] | null;
18};
19
20// Mapping with conditional types
21type Getters<T> = {
22 [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
23};
24
25interface Person {
26 name: string;
27 age: number;
28}
29
30type PersonGetters = Getters<Person>;
31// {
32// getName: () => string;
33// getAge: () => number;
34// }
35
36// Real-world: Form validation types
37type ValidationErrors<T> = {
38 [P in keyof T]?: string[];
39};
40
41type FormState<T> = {
42 values: T;
43 errors: ValidationErrors<T>;
44 touched: { [P in keyof T]?: boolean };
45};
46
47const userForm: FormState<User> = {
48 values: { id: 0, name: '', email: '', role: 'user' },
49 errors: { email: ['Invalid email format'] },
50 touched: { email: true }
51};
Template Literal Types
TypeScript 4.1+ allows creating types from string literals. Perfect for type-safe event systems and APIs.
1// Basic template literal type
2type EventName<T extends string> = `on${Capitalize<T>}`;
3type ClickEvent = EventName<'click'>; // 'onClick'
4
5// Combining with unions
6type Direction = 'left' | 'right' | 'top' | 'bottom';
7type PaddingDirection = `padding${Capitalize<Direction>}`;
8// 'paddingLeft' | 'paddingRight' | 'paddingTop' | 'paddingBottom'
9
10// Real-world: Type-safe event emitter
11type Events = {
12 'user:login': { userId: number; timestamp: Date };
13 'user:logout': { userId: number };
14 'post:create': { postId: number; authorId: number };
15 'post:delete': { postId: number };
16};
17
18type EventKey = keyof Events;
19
20class TypedEventEmitter {
21 private listeners: {
22 [K in EventKey]?: Array<(data: Events[K]) => void>;
23 } = {};
24
25 on<K extends EventKey>(
26 event: K,
27 callback: (data: Events[K]) => void
28 ) {
29 if (!this.listeners[event]) {
30 this.listeners[event] = [];
31 }
32 this.listeners[event]!.push(callback);
33 }
34
35 emit<K extends EventKey>(event: K, data: Events[K]) {
36 this.listeners[event]?.forEach(cb => cb(data));
37 }
38}
39
40const emitter = new TypedEventEmitter();
41
42// ✅ Type-safe: correct payload type
43emitter.on('user:login', (data) => {
44 console.log(data.userId, data.timestamp);
45});
46
47// ❌ Error: Wrong payload type
48// emitter.emit('user:login', { userId: 1 }); // Missing timestamp
49
50// Advanced: API route typing
51type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
52type APIRoute = `/api/${'users' | 'posts'}/${string}`;
53type APIEndpoint = `${HTTPMethod} ${APIRoute}`;
54
55type GetUserEndpoint = Extract<APIEndpoint, `GET /api/users/${string}`>;
56// 'GET /api/users/${string}'
Type Inference with infer Keyword
1// Extract return type from function
2type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
3
4function getUser() {
5 return { id: 1, name: 'John' };
6}
7
8type UserReturn = ReturnType<typeof getUser>;
9// { id: number; name: string; }
10
11// Extract promise resolved type
12type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
13
14type AsyncData = Promise<{ data: string[] }>;
15type SyncData = UnwrapPromise<AsyncData>;
16// { data: string[]; }
17
18// Extract array element type
19type ArrayElement<T> = T extends (infer U)[] ? U : never;
20
21type Numbers = number[];
22type NumberElement = ArrayElement<Numbers>; // number
23
24// Real-world: Extract props from component
25type ComponentProps<T> = T extends React.FC<infer P> ? P : never;
26
27const Button: React.FC<{ label: string; onClick: () => void }> = () => null;
28type ButtonProps = ComponentProps<typeof Button>;
29// { label: string; onClick: () => void; }
Practical Patterns
1// 1. Builder pattern with type safety
2class QueryBuilder<T = {}> {
3 private query: T = {} as T;
4
5 where<K extends string, V>(
6 key: K,
7 value: V
8 ): QueryBuilder<T & Record<K, V>> {
9 return new QueryBuilder<T & Record<K, V>>();
10 }
11
12 execute(): T {
13 return this.query;
14 }
15}
16
17const query = new QueryBuilder()
18 .where('name', 'John')
19 .where('age', 30)
20 .execute();
21// Type: { name: string; age: number; }
22
23// 2. Discriminated unions for state management
24type LoadingState = { status: 'loading' };
25type SuccessState<T> = { status: 'success'; data: T };
26type ErrorState = { status: 'error'; error: string };
27
28type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
29
30function renderState<T>(state: AsyncState<T>) {
31 switch (state.status) {
32 case 'loading':
33 return 'Loading...';
34 case 'success':
35 // TypeScript knows state.data exists
36 return `Data: ${state.data}`;
37 case 'error':
38 // TypeScript knows state.error exists
39 return `Error: ${state.error}`;
40 }
41}
42
43// 3. Branded types for type safety
44type Brand<K, T> = K & { __brand: T };
45type USD = Brand<number, 'USD'>;
46type EUR = Brand<number, 'EUR'>;
47
48function processPayment(amount: USD) {
49 console.log(`Processing $${amount}`);
50}
51
52const dollars = 100 as USD;
53const euros = 100 as EUR;
54
55processPayment(dollars); // ✅
56// processPayment(euros); // ❌ Error: Type 'EUR' not assignable to 'USD'
Key Takeaways
- Utility types (Partial, Pick, Omit, Record) solve 90% of common type transformations
- Conditional types enable types that adapt based on input types
- Mapped types transform existing types systematically
- Template literal types provide type-safe string manipulation
- The infer keyword extracts types from complex type structures
- Discriminated unions with status fields enable exhaustive type checking
- Branded types prevent mixing logically different but structurally identical types
- Type-level programming makes impossible states unrepresentable
- Good types catch bugs at compile time, not runtime