TypeScript’s type system is far more powerful than just string, number, and boolean. By mastering advanced type techniques, you can build code that is both type-safe and elegantly flexible. This article takes you deep into the essence of TypeScript’s type system.
The Art of Type Inference
as const Assertion
// Regular declaration: type is widened
const config = {
endpoint: '/api/users',
method: 'GET'
};
// Type: { endpoint: string; method: string }
// as const: type is narrowed to literals
const config = {
endpoint: '/api/users',
method: 'GET'
} as const;
// Type: { readonly endpoint: "/api/users"; readonly method: "GET" }
as const is extremely useful for defining constant configurations and enum alternatives:
const HttpMethods = ['GET', 'POST', 'PUT', 'DELETE'] as const;
type HttpMethod = typeof HttpMethods[number];
// Type: "GET" | "POST" | "PUT" | "DELETE"
The satisfies Operator
TypeScript 4.9 introduced satisfies, giving you both type checking and precise inference:
type Colors = Record<string, string | number[]>;
// Using type annotation: loses specific info
const colors: Colors = {
red: '#ff0000',
green: [0, 255, 0]
};
colors.red.toUpperCase(); // ❌ Error: might be number[]
// Using satisfies: preserves specific types
const colors = {
red: '#ff0000',
green: [0, 255, 0]
} satisfies Colors;
colors.red.toUpperCase(); // ✅ OK: knows it's string
colors.green.map(n => n); // ✅ OK: knows it's number[]
Conditional Types
Conditional types are the core of TypeScript type programming:
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
The infer Keyword
infer lets you extract types within conditional types:
// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => Promise<string>;
type Result = ReturnType<Fn>; // Promise<string>
// Extract Promise inner type
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type A = Awaited<Promise<Promise<string>>>; // string
// Extract array element type
type ElementType<T> = T extends (infer E)[] ? E : never;
type E = ElementType<string[]>; // string
Practical Conditional Types
// Extract keys whose values are functions
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never
}[keyof T];
interface User {
name: string;
age: number;
greet(): void;
update(data: Partial<User>): void;
}
type UserMethods = FunctionKeys<User>; // "greet" | "update"
Mapped Types
Mapped types let you create new types based on existing ones:
// Basic mapping
type Readonly<T> = {
readonly [K in keyof T]: T[K]
};
type Partial<T> = {
[K in keyof T]?: T[K]
};
type Required<T> = {
[K in keyof T]-?: T[K] // -? removes optional marker
};
Key Remapping
TypeScript 4.1 introduced the as clause for key remapping:
// Add prefix to all keys
type Prefixed<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K]
};
interface User {
name: string;
age: number;
}
type PrefixedUser = Prefixed<User, 'user'>;
// { userName: string; userAge: number }
// Filter keys of specific types
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K]
};
type StringProps = OnlyStrings<User>;
// { name: string }
Getter/Setter Generator
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
};
interface Person {
name: string;
age: number;
}
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }
type PersonSetters = Setters<Person>;
// { setName: (value: string) => void; setAge: (value: number) => void }
Template Literal Types
TypeScript 4.1 brought powerful template literal types:
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // "onClick"
type FocusEvent = EventName<'focus'>; // "onFocus"
// Combining multiple literals
type Vertical = 'top' | 'bottom';
type Horizontal = 'left' | 'right';
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
Built-in String Manipulation Types
type Upper = Uppercase<'hello'>; // "HELLO"
type Lower = Lowercase<'HELLO'>; // "hello"
type Cap = Capitalize<'hello'>; // "Hello"
type Uncap = Uncapitalize<'Hello'>; // "hello"
Practical: Type-Safe Event System
type EventMap = {
click: { x: number; y: number };
focus: { target: HTMLElement };
submit: { data: FormData };
};
type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;
type EventHandlers = {
[K in keyof EventMap as `on${Capitalize<K>}`]: EventHandler<K>
};
// Result:
// {
// onClick: (event: { x: number; y: number }) => void;
// onFocus: (event: { target: HTMLElement }) => void;
// onSubmit: (event: { data: FormData }) => void;
// }
Recursive Types
TypeScript supports recursive type definitions:
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K]
};
// Deep partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K]
};
// Flatten nested object paths
type Paths<T, D extends number = 10> = [D] extends [never]
? never
: T extends object
? {
[K in keyof T]-?: K extends string | number
? `${K}` | `${K}.${Paths<T[K], Prev[D]>}`
: never
}[keyof T]
: never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
interface Config {
server: {
host: string;
port: number;
};
database: {
connection: {
url: string;
};
};
}
type ConfigPaths = Paths<Config>;
// "server" | "server.host" | "server.port" | "database" | "database.connection" | "database.connection.url"
Type Gymnastics in Practice
1. Tuple Operations
// Get first element of tuple
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
// Get last element of tuple
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
// Remove first element
type Shift<T extends any[]> = T extends [any, ...infer R] ? R : never;
// Remove last element
type Pop<T extends any[]> = T extends [...infer R, any] ? R : never;
// Reverse tuple
type Reverse<T extends any[]> = T extends [infer F, ...infer R]
? [...Reverse<R>, F]
: [];
type Tuple = [1, 2, 3, 4, 5];
type FirstEl = First<Tuple>; // 1
type LastEl = Last<Tuple>; // 5
type Shifted = Shift<Tuple>; // [2, 3, 4, 5]
type Popped = Pop<Tuple>; // [1, 2, 3, 4]
type Reversed = Reverse<Tuple>; // [5, 4, 3, 2, 1]
2. Union to Intersection
type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends
(x: infer I) => void ? I : never;
type Union = { a: string } | { b: number } | { c: boolean };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number } & { c: boolean }
3. Strict Object Types
// Exact type, no extra properties allowed
type Exact<T, Shape> = T extends Shape
? Exclude<keyof T, keyof Shape> extends never
? T
: never
: never;
type User = { name: string; age: number };
function createUser<T extends User>(user: Exact<T, User>): User {
return user;
}
createUser({ name: 'Alice', age: 30 }); // ✅
createUser({ name: 'Bob', age: 25, extra: 1 }); // ❌ Type error
Utility Types
Deep Required
type DeepRequired<T> = {
[K in keyof T]-?: T[K] extends object
? DeepRequired<T[K]>
: T[K]
};
Nullable Type Handling
type NonNullableDeep<T> = {
[K in keyof T]: NonNullable<T[K]> extends object
? NonNullableDeep<NonNullable<T[K]>>
: NonNullable<T[K]>
};
Selective Pick and Omit
// Pick only properties of specific type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
};
// Omit properties of specific type
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
};
interface Mixed {
name: string;
age: number;
active: boolean;
email: string;
}
type StringProps = PickByType<Mixed, string>;
// { name: string; email: string }
type NonBooleanProps = OmitByType<Mixed, boolean>;
// { name: string; age: number; email: string }
Type Assertion Functions
// Assertion function
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());
}
// Type guard
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'name' in obj &&
'age' in obj
);
}
function greet(input: unknown) {
if (isUser(input)) {
// input is typed as User
console.log(`Hello, ${input.name}!`);
}
}
Best Practices
1. Avoid Overusing any
// ❌ Avoid
function process(data: any): any {
return data.value;
}
// ✅ Use generics
function process<T extends { value: unknown }>(data: T): T['value'] {
return data.value;
}
2. Prefer unknown Over any
// ❌ any allows any operation
function dangerous(value: any) {
value.foo.bar.baz(); // No error, but might crash at runtime
}
// ✅ unknown forces type checking
function safe(value: unknown) {
if (typeof value === 'object' && value !== null && 'foo' in value) {
// Safe access
}
}
3. Leverage const Generics
function createConfig<const T extends Record<string, unknown>>(config: T): T {
return config;
}
const config = createConfig({
api: '/api',
timeout: 5000
});
// Type: { readonly api: "/api"; readonly timeout: 5000 }
Summary
TypeScript advanced types are a double-edged sword:
| Scenario | Recommendation |
|---|---|
| Library/Framework dev | Fully leverage advanced types for best DX |
| Business code | Use moderately, maintain readability |
| Type gymnastics | Learn principles, use cautiously in production |
Key Takeaways:
as constandsatisfiesimprove type precision- Conditional types +
inferare the core of type programming - Mapped types + key remapping enable type transformations
- Template literal types handle string types
- Recursive types handle nested structures
Master these advanced type techniques, and your TypeScript code will be more type-safe and expressive. Remember: the type system is your friend, not your enemy.
Types are documentation, types are tests, types are your safety net. Let TypeScript’s type system guard your code.