TypeScript Advanced Types: From Utility Types to Template Literals
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.
// Partial<T> - Makes all properties optional
interface User {
id: number;
name: string;
email: string;
role: 'admin' | 'user';
}
// Update function accepts partial user data
function updateUser(id: number, updates: Partial<User>) {
// Implementation
}
updateUser(1, { email: 'new@email.com' }); // ✅ Only email needed
// Required<T> - Makes all properties required
type UserWithDefaults = Partial<User>;
type CompleteUser = Required<UserWithDefaults>; // All fields required again
// Readonly<T> - Makes all properties immutable
const config: Readonly<User> = {
id: 1,
name: 'Admin',
email: 'admin@app.com',
role: 'admin'
};
// config.email = 'test'; // ❌ Error: Cannot assign to 'email'
// Pick<T, K> - Select specific properties
type UserPublicInfo = Pick<User, 'name' | 'email'>;
// { name: string; email: string; }
// Omit<T, K> - Remove specific properties
type UserWithoutId = Omit<User, 'id'>;
// { name: string; email: string; role: 'admin' | 'user'; }
// Record<K, T> - Create object type with specific keys
type UserRoles = Record<'admin' | 'user' | 'guest', string[]>;
const permissions: UserRoles = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
};Conditional Types: Type-Level if Statements
Conditional types enable types that change based on conditions. They unlock powerful type inference patterns.
// Basic conditional type syntax: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Real-world example: Type-safe API response handler
type ApiResponse<T> = T extends { error: any }
? { success: false; error: string }
: { success: true; data: T };
function handleResponse<T>(response: ApiResponse<T>) {
if (response.success) {
// TypeScript knows response.data exists here
console.log(response.data);
} else {
// TypeScript knows response.error exists here
console.error(response.error);
}
}
// Exclude<T, U> - Remove types from union
type AllTypes = 'click' | 'focus' | 'blur' | 'submit';
type ClickTypes = Exclude<AllTypes, 'focus' | 'blur'>; // 'click' | 'submit'
// Extract<T, U> - Extract types from union
type EventNames = Extract<AllTypes, 'click' | 'submit'>; // 'click' | 'submit'
// NonNullable<T> - Remove null and undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>; // string
// Advanced: Recursive conditional types
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface NestedConfig {
database: {
host: string;
credentials: {
username: string;
password: string;
};
};
}
type ImmutableConfig = DeepReadonly<NestedConfig>;
// All nested properties are readonlyMapped Types: Transform Existing Types
// Basic mapped type
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
interface Post {
id: number;
title: string;
body: string;
}
type NullablePost = Nullable<Post>;
// { id: number | null; title: string | null; body: string | null; }
// With modifiers: readonly and optional
type ReadonlyNullable<T> = {
readonly [P in keyof T]?: T[P] | null;
};
// Mapping with conditional types
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// {
// getName: () => string;
// getAge: () => number;
// }
// Real-world: Form validation types
type ValidationErrors<T> = {
[P in keyof T]?: string[];
};
type FormState<T> = {
values: T;
errors: ValidationErrors<T>;
touched: { [P in keyof T]?: boolean };
};
const userForm: FormState<User> = {
values: { id: 0, name: '', email: '', role: 'user' },
errors: { email: ['Invalid email format'] },
touched: { email: true }
};Template Literal Types
TypeScript 4.1+ allows creating types from string literals. Perfect for type-safe event systems and APIs.
// Basic template literal type
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
// Combining with unions
type Direction = 'left' | 'right' | 'top' | 'bottom';
type PaddingDirection = `padding${Capitalize<Direction>}`;
// 'paddingLeft' | 'paddingRight' | 'paddingTop' | 'paddingBottom'
// Real-world: Type-safe event emitter
type Events = {
'user:login': { userId: number; timestamp: Date };
'user:logout': { userId: number };
'post:create': { postId: number; authorId: number };
'post:delete': { postId: number };
};
type EventKey = keyof Events;
class TypedEventEmitter {
private listeners: {
[K in EventKey]?: Array<(data: Events[K]) => void>;
} = {};
on<K extends EventKey>(
event: K,
callback: (data: Events[K]) => void
) {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends EventKey>(event: K, data: Events[K]) {
this.listeners[event]?.forEach(cb => cb(data));
}
}
const emitter = new TypedEventEmitter();
// ✅ Type-safe: correct payload type
emitter.on('user:login', (data) => {
console.log(data.userId, data.timestamp);
});
// ❌ Error: Wrong payload type
// emitter.emit('user:login', { userId: 1 }); // Missing timestamp
// Advanced: API route typing
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIRoute = `/api/${'users' | 'posts'}/${string}`;
type APIEndpoint = `${HTTPMethod} ${APIRoute}`;
type GetUserEndpoint = Extract<APIEndpoint, `GET /api/users/${string}`>;
// 'GET /api/users/${string}'Type Inference with infer Keyword
// Extract return type from function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: 'John' };
}
type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string; }
// Extract promise resolved type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type AsyncData = Promise<{ data: string[] }>;
type SyncData = UnwrapPromise<AsyncData>;
// { data: string[]; }
// Extract array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Numbers = number[];
type NumberElement = ArrayElement<Numbers>; // number
// Real-world: Extract props from component
type ComponentProps<T> = T extends React.FC<infer P> ? P : never;
const Button: React.FC<{ label: string; onClick: () => void }> = () => null;
type ButtonProps = ComponentProps<typeof Button>;
// { label: string; onClick: () => void; }Practical Patterns
// 1. Builder pattern with type safety
class QueryBuilder<T = {}> {
private query: T = {} as T;
where<K extends string, V>(
key: K,
value: V
): QueryBuilder<T & Record<K, V>> {
return new QueryBuilder<T & Record<K, V>>();
}
execute(): T {
return this.query;
}
}
const query = new QueryBuilder()
.where('name', 'John')
.where('age', 30)
.execute();
// Type: { name: string; age: number; }
// 2. Discriminated unions for state management
type LoadingState = { status: 'loading' };
type SuccessState<T> = { status: 'success'; data: T };
type ErrorState = { status: 'error'; error: string };
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState;
function renderState<T>(state: AsyncState<T>) {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'success':
// TypeScript knows state.data exists
return `Data: ${state.data}`;
case 'error':
// TypeScript knows state.error exists
return `Error: ${state.error}`;
}
}
// 3. Branded types for type safety
type Brand<K, T> = K & { __brand: T };
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;
function processPayment(amount: USD) {
console.log(`Processing $${amount}`);
}
const dollars = 100 as USD;
const euros = 100 as EUR;
processPayment(dollars); // ✅
// 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