TypeScript Advanced Types
Comprehensive guidance for mastering TypeScript's advanced type system including generics, conditional types, mapped types, template literal types, and utility types for building robust, type-safe applications.
When to Use This Skill
-
Building type-safe libraries or frameworks
-
Creating reusable generic components
-
Implementing complex type inference logic
-
Designing type-safe API clients
-
Building form validation systems
-
Creating strongly-typed configuration objects
-
Implementing type-safe state management
-
Migrating JavaScript codebases to TypeScript
Core Concepts
- Generics
Purpose: Create reusable, type-flexible components while maintaining type safety.
Basic Generic Function:
function identity<T>(value: T): T { return value; }
const num = identity<number>(42); // Type: number const str = identity<string>("hello"); // Type: string const auto = identity(true); // Type inferred: boolean
Generic Constraints:
interface HasLength { length: number; }
function logLength<T extends HasLength>(item: T): T { console.log(item.length); return item; }
logLength("hello"); // OK: string has length logLength([1, 2, 3]); // OK: array has length logLength({ length: 10 }); // OK: object has length // logLength(42); // Error: number has no length
Multiple Type Parameters:
function merge<T, U>(obj1: T, obj2: U): T & U { return { ...obj1, ...obj2 }; }
const merged = merge({ name: "John" }, { age: 30 }); // Type: { name: string } & { age: number }
- Conditional Types
Purpose: Create types that depend on conditions, enabling sophisticated type logic.
Basic Conditional Type:
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true type B = IsString<number>; // false
Extracting Return Types:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { id: 1, name: "John" }; }
type User = ReturnType<typeof getUser>; // Type: { id: number; name: string; }
Distributive Conditional Types:
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>; // Type: string[] | number[]
Nested Conditions:
type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object";
type T1 = TypeName<string>; // "string" type T2 = TypeName<() => void>; // "function"
- Mapped Types
Purpose: Transform existing types by iterating over their properties.
Basic Mapped Type:
type Readonly<T> = { readonly [P in keyof T]: T[P]; };
interface User { id: number; name: string; }
type ReadonlyUser = Readonly<User>; // Type: { readonly id: number; readonly name: string; }
Optional Properties:
type Partial<T> = { [P in keyof T]?: T[P]; };
type PartialUser = Partial<User>; // Type: { id?: number; name?: string; }
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>; // Type: { getName: () => string; getAge: () => number; }
Filtering Properties:
type PickByType<T, U> = { [K in keyof T as T[K] extends U ? K : never]: T[K]; };
interface Mixed { id: number; name: string; age: number; active: boolean; }
type OnlyNumbers = PickByType<Mixed, number>; // Type: { id: number; age: number; }
- Template Literal Types
Purpose: Create string-based types with pattern matching and transformation.
Basic Template Literal:
type EventName = "click" | "focus" | "blur";
type EventHandler = on${Capitalize<EventName>};
// Type: "onClick" | "onFocus" | "onBlur"
String Manipulation:
type UppercaseGreeting = Uppercase<"hello">; // "HELLO" type LowercaseGreeting = Lowercase<"HELLO">; // "hello" type CapitalizedName = Capitalize<"john">; // "John" type UncapitalizedName = Uncapitalize<"John">; // "john"
Path Building:
type Path<T> = T extends object
? {
[K in keyof T]: K extends string ? ${K} | ${K}.${Path<T[K]>} : never;
}[keyof T]
: never;
interface Config { server: { host: string; port: number; }; database: { url: string; }; }
type ConfigPath = Path<Config>; // Type: "server" | "database" | "server.host" | "server.port" | "database.url"
- Utility Types
Built-in Utility Types:
// Partial<T> - Make all properties optional type PartialUser = Partial<User>;
// Required<T> - Make all properties required type RequiredUser = Required<PartialUser>;
// Readonly<T> - Make all properties readonly type ReadonlyUser = Readonly<User>;
// Pick<T, K> - Select specific properties type UserName = Pick<User, "name" | "email">;
// Omit<T, K> - Remove specific properties type UserWithoutPassword = Omit<User, "password">;
// Exclude<T, U> - Exclude types from union type T1 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
// Extract<T, U> - Extract types from union type T2 = Extract<"a" | "b" | "c", "a" | "b">; // "a" | "b"
// NonNullable<T> - Exclude null and undefined type T3 = NonNullable<string | null | undefined>; // string
// Record<K, T> - Create object type with keys K and values T type PageInfo = Record<"home" | "about", { title: string }>;
Advanced Patterns
Pattern 1: Type-Safe Event Emitter
type EventMap = { "user:created": { id: string; name: string }; "user:updated": { id: string }; "user:deleted": { id: string }; };
class TypedEventEmitter<T extends Record<string, any>> { private listeners: { [K in keyof T]?: Array<(data: T[K]) => void>; } = {};
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event]!.push(callback); }
emit<K extends keyof T>(event: K, data: T[K]): void { const callbacks = this.listeners[event]; if (callbacks) { callbacks.forEach((callback) => callback(data)); } } }
const emitter = new TypedEventEmitter<EventMap>();
emitter.on("user:created", (data) => { console.log(data.id, data.name); // Type-safe! });
emitter.emit("user:created", { id: "1", name: "John" }); // emitter.emit("user:created", { id: "1" }); // Error: missing 'name'
Pattern 2: Type-Safe API Client
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type EndpointConfig = { "/users": { GET: { response: User[] }; POST: { body: { name: string; email: string }; response: User }; }; "/users/:id": { GET: { params: { id: string }; response: User }; PUT: { params: { id: string }; body: Partial<User>; response: User }; DELETE: { params: { id: string }; response: void }; }; };
type ExtractParams<T> = T extends { params: infer P } ? P : never; type ExtractBody<T> = T extends { body: infer B } ? B : never; type ExtractResponse<T> = T extends { response: infer R } ? R : never;
class APIClient<Config extends Record<string, Record<HTTPMethod, any>>> { async request<Path extends keyof Config, Method extends keyof Config[Path]>( path: Path, method: Method, ...[options]: ExtractParams<Config[Path][Method]> extends never ? ExtractBody<Config[Path][Method]> extends never ? [] : [{ body: ExtractBody<Config[Path][Method]> }] : [ { params: ExtractParams<Config[Path][Method]>; body?: ExtractBody<Config[Path][Method]>; }, ] ): Promise<ExtractResponse<Config[Path][Method]>> { // Implementation here return {} as any; } }
const api = new APIClient<EndpointConfig>();
// Type-safe API calls const users = await api.request("/users", "GET"); // Type: User[]
const newUser = await api.request("/users", "POST", { body: { name: "John", email: "john@example.com" }, }); // Type: User
const user = await api.request("/users/:id", "GET", { params: { id: "123" }, }); // Type: User
Pattern 3: Builder Pattern with Type Safety
type BuilderState<T> = { [K in keyof T]: T[K] | undefined; };
type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K; }[keyof T];
type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never; }[keyof T];
type IsComplete<T, S> = RequiredKeys<T> extends keyof S ? S[RequiredKeys<T>] extends undefined ? false : true : false;
class Builder<T, S extends BuilderState<T> = {}> { private state: S = {} as S;
set<K extends keyof T>(key: K, value: T[K]): Builder<T, S & Record<K, T[K]>> { this.state[key] = value; return this as any; }
build(this: IsComplete<T, S> extends true ? this : never): T { return this.state as T; } }
interface User { id: string; name: string; email: string; age?: number; }
const builder = new Builder<User>();
const user = builder .set("id", "1") .set("name", "John") .set("email", "john@example.com") .build(); // OK: all required fields set
// const incomplete = builder // .set("id", "1") // .build(); // Error: missing required fields
Pattern 4: Deep Readonly/Partial
type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? T[P] extends Function ? T[P] : DeepReadonly<T[P]> : T[P]; };
type DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? T[P] extends Array<infer U> ? Array<DeepPartial<U>> : DeepPartial<T[P]> : T[P]; };
interface Config { server: { host: string; port: number; ssl: { enabled: boolean; cert: string; }; }; database: { url: string; pool: { min: number; max: number; }; }; }
type ReadonlyConfig = DeepReadonly<Config>; // All nested properties are readonly
type PartialConfig = DeepPartial<Config>; // All nested properties are optional
Pattern 5: Type-Safe Form Validation
type ValidationRule<T> = { validate: (value: T) => boolean; message: string; };
type FieldValidation<T> = { [K in keyof T]?: ValidationRule<T[K]>[]; };
type ValidationErrors<T> = { [K in keyof T]?: string[]; };
class FormValidator<T extends Record<string, any>> { constructor(private rules: FieldValidation<T>) {}
validate(data: T): ValidationErrors<T> | null { const errors: ValidationErrors<T> = {}; let hasErrors = false;
for (const key in this.rules) {
const fieldRules = this.rules[key];
const value = data[key];
if (fieldRules) {
const fieldErrors: string[] = [];
for (const rule of fieldRules) {
if (!rule.validate(value)) {
fieldErrors.push(rule.message);
}
}
if (fieldErrors.length > 0) {
errors[key] = fieldErrors;
hasErrors = true;
}
}
}
return hasErrors ? errors : null;
} }
interface LoginForm { email: string; password: string; }
const validator = new FormValidator<LoginForm>({ email: [ { validate: (v) => v.includes("@"), message: "Email must contain @", }, { validate: (v) => v.length > 0, message: "Email is required", }, ], password: [ { validate: (v) => v.length >= 8, message: "Password must be at least 8 characters", }, ], });
const errors = validator.validate({ email: "invalid", password: "short", }); // Type: { email?: string[]; password?: string[]; } | null
Pattern 6: Discriminated Unions
type Success<T> = { status: "success"; data: T; };
type Error = { status: "error"; error: string; };
type Loading = { status: "loading"; };
type AsyncState<T> = Success<T> | Error | Loading;
function handleState<T>(state: AsyncState<T>): void { switch (state.status) { case "success": console.log(state.data); // Type: T break; case "error": console.log(state.error); // Type: string break; case "loading": console.log("Loading..."); break; } }
// Type-safe state machine type State = | { type: "idle" } | { type: "fetching"; requestId: string } | { type: "success"; data: any } | { type: "error"; error: Error };
type Event = | { type: "FETCH"; requestId: string } | { type: "SUCCESS"; data: any } | { type: "ERROR"; error: Error } | { type: "RESET" };
function reducer(state: State, event: Event): State { switch (state.type) { case "idle": return event.type === "FETCH" ? { type: "fetching", requestId: event.requestId } : state; case "fetching": if (event.type === "SUCCESS") { return { type: "success", data: event.data }; } if (event.type === "ERROR") { return { type: "error", error: event.error }; } return state; case "success": case "error": return event.type === "RESET" ? { type: "idle" } : state; } }
Type Inference Techniques
- Infer Keyword
// Extract array element type type ElementType<T> = T extends (infer U)[] ? U : never;
type NumArray = number[]; type Num = ElementType<NumArray>; // number
// Extract promise type type PromiseType<T> = T extends Promise<infer U> ? U : never;
type AsyncNum = PromiseType<Promise<number>>; // number
// Extract function parameters type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function foo(a: string, b: number) {} type FooParams = Parameters<typeof foo>; // [string, number]
- Type Guards
function isString(value: unknown): value is string { return typeof value === "string"; }
function isArrayOf<T>( value: unknown, guard: (item: unknown) => item is T, ): value is T[] { return Array.isArray(value) && value.every(guard); }
const data: unknown = ["a", "b", "c"];
if (isArrayOf(data, isString)) { data.forEach((s) => s.toUpperCase()); // Type: string[] }
- Assertion Functions
function assertIsString(value: unknown): asserts value is string { if (typeof value !== "string") { throw new Error("Not a string"); } }
function processValue(value: unknown) { assertIsString(value); // value is now typed as string console.log(value.toUpperCase()); }
Best Practices
-
Use unknown over any : Enforce type checking
-
Prefer interface for object shapes: Better error messages
-
Use type for unions and complex types: More flexible
-
Leverage type inference: Let TypeScript infer when possible
-
Create helper types: Build reusable type utilities
-
Use const assertions: Preserve literal types
-
Avoid type assertions: Use type guards instead
-
Document complex types: Add JSDoc comments
-
Use strict mode: Enable all strict compiler options
-
Test your types: Use type tests to verify type behavior
Type Testing
// Type assertion tests type AssertEqual<T, U> = [T] extends [U] ? [U] extends [T] ? true : false : false;
type Test1 = AssertEqual<string, string>; // true type Test2 = AssertEqual<string, number>; // false type Test3 = AssertEqual<string | number, string>; // false
// Expect error helper type ExpectError<T extends never> = T;
// Example usage type ShouldError = ExpectError<AssertEqual<string, number>>;
Common Pitfalls
-
Over-using any : Defeats the purpose of TypeScript
-
Ignoring strict null checks: Can lead to runtime errors
-
Too complex types: Can slow down compilation
-
Not using discriminated unions: Misses type narrowing opportunities
-
Forgetting readonly modifiers: Allows unintended mutations
-
Circular type references: Can cause compiler errors
-
Not handling edge cases: Like empty arrays or null values
Performance Considerations
-
Avoid deeply nested conditional types
-
Use simple types when possible
-
Cache complex type computations
-
Limit recursion depth in recursive types
-
Use build tools to skip type checking in production
Resources
-
TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/
-
Type Challenges: https://github.com/type-challenges/type-challenges
-
TypeScript Deep Dive: https://basarat.gitbook.io/typescript/
-
Effective TypeScript: Book by Dan Vanderkam