All posts
TypeScript

TypeScript Advanced Types: From Utility Types to Template Literals

15 min
Share:
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