Advanced TypeScript: Mastering Type System for Better Code
TypeScript’s type system is incredibly powerful. Let’s explore advanced features that will take your TypeScript skills to the next level.
Generics
Basic Generics
function identity<T>(arg: T): T {
return arg;
}
const num = identity<number>(42);
const str = identity<string>("hello");
Generic Constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}
logLength("hello"); // ✅
logLength([1, 2, 3]); // ✅
logLength({ length: 10 }); // ✅
// logLength(123); // ❌ Error
Multiple Type Parameters
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}
const result = merge(
{ name: "John" },
{ age: 30 }
);
// result: { name: string; age: number }
Conditional Types
Basic Conditional Types
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
Infer Keyword
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { name: "John", age: 30 };
}
type User = ReturnType<typeof getUser>;
// { name: string; age: number }
Distributive Conditional Types
type ToArray<T> = T extends any ? T[] : never;
type StrOrNumArray = ToArray<string | number>;
// string[] | number[]
Mapped Types
Basic Mapped Types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
type PartialUser = Partial<User>;
Template Literal Types
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;
// }
Utility Types
Built-in Utility Types
// Pick - Select specific properties
type UserPreview = Pick<User, 'name' | 'email'>;
// Omit - Exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;
// Record - Create object type
type PageInfo = Record<'home' | 'about' | 'contact', { title: string }>;
// Exclude - Exclude from union
type T1 = Exclude<'a' | 'b' | 'c', 'a'>; // 'b' | 'c'
// Extract - Extract from union
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'
// NonNullable - Remove null and undefined
type T3 = NonNullable<string | null | undefined>; // string
Custom Utility Types
// Deep Partial
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Required Recursive
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
// Mutable (remove readonly)
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
Type Guards
User-Defined Type Guards
interface Fish {
swim: () => void;
}
interface Bird {
fly: () => void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) {
pet.swim(); // TypeScript knows it's Fish
} else {
pet.fly(); // TypeScript knows it's Bird
}
}
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);
// TypeScript now knows value is string
console.log(value.toUpperCase());
}
Advanced Patterns
Discriminated Unions
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number }
| { kind: 'rectangle'; width: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.size ** 2;
case 'rectangle':
return shape.width * shape.height;
}
}
Branded Types
type Brand<K, T> = K & { __brand: T };
type USD = Brand<number, 'USD'>;
type EUR = Brand<number, 'EUR'>;
function usd(amount: number): USD {
return amount as USD;
}
function eur(amount: number): EUR {
return amount as EUR;
}
const dollars = usd(100);
const euros = eur(100);
// const sum = dollars + euros; // ❌ Type error!
Builder Pattern with Types
class RequestBuilder<T extends { url?: string; method?: string; body?: any }> {
private config: T;
constructor(config: T) {
this.config = config;
}
url<U extends string>(url: U): RequestBuilder<T & { url: U }> {
return new RequestBuilder({ ...this.config, url });
}
method<M extends string>(method: M): RequestBuilder<T & { method: M }> {
return new RequestBuilder({ ...this.config, method });
}
body<B>(body: B): RequestBuilder<T & { body: B }> {
return new RequestBuilder({ ...this.config, body });
}
build(this: RequestBuilder<{ url: string; method: string }>): T {
return this.config;
}
}
const request = new RequestBuilder({})
.url('/api/users')
.method('POST')
.body({ name: 'John' })
.build(); // ✅ All required fields present
// const incomplete = new RequestBuilder({})
// .url('/api/users')
// .build(); // ❌ Error: method is required
Type-Level Programming
Recursive Types
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
const data: Json = {
name: "John",
age: 30,
hobbies: ["reading", "coding"],
address: {
city: "NYC",
zip: 10001
}
};
Type-Level Functions
// Get function parameter types
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) {
return `Hello ${name}, ${age}`;
}
type GreetParams = Parameters<typeof greet>;
// [name: string, age: number]
Real-World Example
// API Response Handler
type ApiResponse<T> =
| { status: 'success'; data: T }
| { status: 'error'; error: string };
async function fetchUser(id: string): Promise<ApiResponse<User>> {
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return { status: 'success', data };
} catch (error) {
return { status: 'error', error: error.message };
}
}
// Type-safe usage
const result = await fetchUser('123');
if (result.status === 'success') {
console.log(result.data.name); // ✅ TypeScript knows data exists
} else {
console.error(result.error); // ✅ TypeScript knows error exists
}
Best Practices
- Use strict mode: Enable all strict options in
tsconfig.json - Prefer type inference: Let TypeScript infer types when possible
- Avoid
any: Useunknowninstead - Use const assertions: For literal types
- Leverage utility types: Don’t reinvent the wheel
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Conclusion
TypeScript’s advanced type system enables you to write safer, more maintainable code. Master these concepts to catch errors at compile-time, improve code documentation, and enhance developer experience.
TypeScriptJavaScriptWeb DevelopmentType SafetyProgramming