# TypeScript Advanced Types: From Beginner to Expert
Master generics, conditional types, mapped types, type inference and utility types
TypeScript’s type system is its core advantage. This article explores advanced type techniques and practical patterns.
Generics Basics
Generic Functions and Classes
// Generic function
function identity<T>(value: T): T {
return value;
}
// Generic constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// Generic class
class Container<T> {
private value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
map<U>(fn: (value: T) => U): Container<U> {
return new Container(fn(this.value));
}
}
// Usage
const numContainer = new Container(42);
const strContainer = numContainer.map(n => n.toString());
Generic Interfaces
// Generic interface
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(data: Omit<T, 'id'>): Promise<T>;
update(id: string, data: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
// Implementation
interface User {
id: string;
name: string;
email: string;
}
class UserRepository implements Repository<User> {
async findById(id: string): Promise<User | null> {
return db.users.findUnique({ where: { id } });
}
async findAll(): Promise<User[]> {
return db.users.findMany();
}
async create(data: Omit<User, 'id'>): Promise<User> {
return db.users.create({ data });
}
async update(id: string, data: Partial<User>): Promise<User> {
return db.users.update({ where: { id }, data });
}
async delete(id: string): Promise<void> {
await db.users.delete({ where: { id } });
}
}
Conditional Types
Basic Conditional Types
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Distributive conditional types
type NonNullable<T> = T extends null | undefined ? never : T;
type C = NonNullable<string | null | undefined>; // string
// Extract and Exclude
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
type D = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type E = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
Type Inference (infer)
// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function createUser() {
return { id: '1', name: 'John' };
}
type User = ReturnType<typeof createUser>; // { id: string; name: string }
// Extract function parameter types
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = Parameters<(a: string, b: number) => void>; // [string, number]
// Extract Promise value type
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type F = Awaited<Promise<Promise<string>>>; // string
// Extract array element type
type ElementType<T> = T extends (infer U)[] ? U : never;
type G = ElementType<string[]>; // string
// Extract object property type
type PropertyType<T, K extends keyof T> = T[K];
interface Person {
name: string;
age: number;
}
type NameType = PropertyType<Person, 'name'>; // string
Mapped Types
Built-in Mapped Types
// Partial: all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Required: all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Readonly: all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Record: key-value mapping
type Record<K extends keyof any, T> = {
[P in K]: T;
};
// Pick: select specific properties
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Omit: exclude specific properties
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Custom Mapped Types
// Deep Partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Deep Readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Nullable type
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
// Key remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
// Filter properties
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
type StringProps = FilterByType<Person, string>; // { name: string }
Template Literal Types
// Basic template literal
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
// Combining types
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiPath = '/users' | '/posts';
type ApiEndpoint = `${HttpMethod} ${ApiPath}`;
// 'GET /users' | 'GET /posts' | 'POST /users' | ...
// CSS units
type CSSValue = `${number}${'px' | 'rem' | 'em' | '%'}`;
const width: CSSValue = '100px'; // OK
const height: CSSValue = '50rem'; // OK
// Type-safe event handlers
type EventHandler<T extends string> = {
[K in T as `on${Capitalize<K>}`]: (event: Event) => void;
};
type ButtonEvents = EventHandler<'click' | 'hover' | 'focus'>;
// { onClick: ...; onHover: ...; onFocus: ... }
Practical Type Patterns
Type Guards
// Type predicates
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
);
}
// Usage
function processValue(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()); // value is string
}
if (isUser(value)) {
console.log(value.name); // value is User
}
}
// Assertion functions
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new Error('Value is not a string');
}
}
function processInput(input: unknown) {
assertIsString(input);
console.log(input.toUpperCase()); // input is string
}
Branded Types
// Branded types prevent type confusion
type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;
function getUser(id: UserId): User {
// ...
}
function getPost(id: PostId): Post {
// ...
}
const userId = 'user-123' as UserId;
const postId = 'post-456' as PostId;
getUser(userId); // OK
getUser(postId); // Error: type mismatch
// Factory functions for branded types
function createUserId(id: string): UserId {
return id as UserId;
}
function createPostId(id: string): PostId {
return id as PostId;
}
Discriminated Unions
// Discriminated union
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
function parseJSON<T>(json: string): Result<T> {
try {
return { success: true, data: JSON.parse(json) };
} catch (e) {
return { success: false, error: e as Error };
}
}
const result = parseJSON<{ name: string }>('{"name": "John"}');
if (result.success) {
console.log(result.data.name); // Type-safe access
} else {
console.error(result.error.message);
}
// State machine types
type LoadingState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function renderState<T>(state: LoadingState<T>) {
switch (state.status) {
case 'idle':
return 'Ready to load';
case 'loading':
return 'Loading...';
case 'success':
return `Data: ${JSON.stringify(state.data)}`;
case 'error':
return `Error: ${state.error.message}`;
}
}
Type Gymnastics in Practice
Implementing Built-in Types
// Implement Readonly
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Implement Pick
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// Implement Exclude
type MyExclude<T, U> = T extends U ? never : T;
// Implement ReturnType
type MyReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : never;
// Implement Awaited
type MyAwaited<T> =
T extends Promise<infer U>
? U extends Promise<any>
? MyAwaited<U>
: U
: T;
// Implement TupleToUnion
type TupleToUnion<T extends readonly any[]> = T[number];
type H = TupleToUnion<['a', 'b', 'c']>; // 'a' | 'b' | 'c'
// Implement First
type First<T extends any[]> = T extends [infer F, ...any] ? F : never;
type I = First<[1, 2, 3]>; // 1
// Implement Last
type Last<T extends any[]> = T extends [...any, infer L] ? L : never;
type J = Last<[1, 2, 3]>; // 3
String Manipulation Types
// Uppercase string
type Uppercase<S extends string> = intrinsic;
// Lowercase string
type Lowercase<S extends string> = intrinsic;
// Capitalize first letter
type Capitalize<S extends string> = intrinsic;
// Uncapitalize first letter
type Uncapitalize<S extends string> = intrinsic;
// Custom: camelCase to kebab-case
type CamelToKebab<S extends string> =
S extends `${infer First}${infer Rest}`
? Rest extends Uncapitalize<Rest>
? `${Lowercase<First>}${CamelToKebab<Rest>}`
: `${Lowercase<First>}-${CamelToKebab<Rest>}`
: S;
type K = CamelToKebab<'backgroundColor'>; // 'background-color'
Best Practices Summary
TypeScript Type Best Practices:
┌─────────────────────────────────────────────────────┐
│ │
│ Type Design │
│ ├── Prefer type inference │
│ ├── Avoid using any │
│ ├── Use unknown instead of any │
│ └── Define precise type boundaries │
│ │
│ Generics Usage │
│ ├── Use appropriate constraints │
│ ├── Avoid over-generalization │
│ ├── Leverage inference to reduce params │
│ └── Use default generic parameters │
│ │
│ Type Organization │
│ ├── Extract common utility types │
│ ├── Use namespaces for related types │
│ ├── Export reusable type definitions │
│ └── Separate type files from implementation │
│ │
└─────────────────────────────────────────────────────┘
| Scenario | Recommended Approach |
|---|---|
| API Responses | Generics + Discriminated Unions |
| Config Objects | Partial + DeepPartial |
| Event Systems | Template Literals + Mapped Types |
| State Management | Discriminated Unions + Type Guards |
| ID Types | Branded Types |
TypeScript’s type system is a language within a language. Master advanced types to make your code safer and more expressive.
Types are the best documentation. Let the compiler be your most faithful code reviewer.