Advanced TypeScript Patterns: Type-Safe Code That Scales
Source: Dev.to

Generic Constraints and Inference
Basic Generics with Constraints
// Constrain a generic to objects that have an `id` property
interface HasId {
id: string | number;
}
/**
* Find an element by its `id`.
* @param items - Array of items that extend `HasId`
* @param id - The id to look for (must be the same type as the item's id)
*/
function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
return items.find(item => item.id === id);
}
// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
id: number;
name: string;
email: string;
}
const users: User[] = [
{ id: 1, name: 'John', email: 'john@example.com' },
{ id: 2, name: 'Jane', email: 'jane@example.com' },
];
const user = findById(users, 1); // Type: User | undefined
Generic Factory Functions
/**
* Simple store implementation with type‑safe state handling.
* @param initialState - The initial state of the store; its type is inferred.
*/
function createStore<T>(initialState: T) {
let state = initialState;
const listeners = new Set<(s: T) => void>();
return {
/** Returns the current state */
getState: () => state,
/**
* Merges a partial update into the current state.
* @param newState - Partial object containing the fields to update.
*/
setState: (newState: Partial<T>) => {
state = { ...state, ...newState };
listeners.forEach(listener => listener(state));
},
/**
* Subscribes a listener that will be called on every state change.
* @param listener - Function receiving the new state.
* @returns A function to unsubscribe the listener.
*/
subscribe: (listener: (state: T) => void) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
id: number;
name: string;
email: string;
}
const userStore = createStore({
user: null as User | null,
isLoading: false,
error: null as string | null,
});
userStore.setState({ isLoading: true }); // ✅ Type‑safe
Tip: TypeScript can infer complex types from usage. Let the compiler do the heavy lifting for internal implementations, but always add explicit generic constraints (e.g.,
T extends HasId) for public APIs to keep them clear and safe.
Conditional Types
Basic Conditional Types
// Generic conditional type that maps a key to a response type
type ApiResponse<T> = T extends 'user'
? User
: T extends 'product'
? Product
: never;
// Example payload types
interface User {
id: number;
name: string;
}
interface Product {
id: number;
price: number;
}
// Usage
type UserResponse = ApiResponse<'user'>; // → User
type ProductResponse = ApiResponse<'product'>; // → Product
Practical example – unwrapping Promise types
// Simplified version of the built‑in `Awaited` type
type Unwrap<T> = T extends Promise<infer R> ? Unwrap<R> : T;
// Example
type Result = Unwrap<Promise<Promise<string>>>; // → string
Distributive Conditional Types
Conditional types automatically distribute over union types. This makes them handy for filtering or transforming each member of a union individually.
Removing null / undefined
// Remove `null` and `undefined` from a type
type NonNullable<T> = T extends null | undefined ? never : T;
// Example
type Example = NonNullable<string | null | undefined>; // → string
Extracting specific members from a union
// Keep only the `string` members of a union
type ExtractStrings<T> = T extends string ? T : never;
type Mixed = string | number | boolean | 'hello' | 'world';
type OnlyStrings = ExtractStrings<Mixed>; // → string | 'hello' | 'world'
Note: Because the conditional type
ExtractStrings<T>is applied to each member ofMixedseparately, the result is a union of the members that satisfy theextends stringcheck. This distributive behavior is what enables powerful type‑level filtering in TypeScript.
Infer Keyword for Type Extraction
The infer keyword lets you capture a type from within a conditional type and reuse it elsewhere. Below are the most common patterns for extracting information from types.
// ------------------------------------------------------------
// 1️⃣ Extract a function’s parameter types
// ------------------------------------------------------------
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
// ------------------------------------------------------------
// 2️⃣ Extract a function’s return type
// ------------------------------------------------------------
type Return<T> = T extends (...args: any[]) => infer R ? R : never;
// ------------------------------------------------------------
// 3️⃣ Extract the element type of an array (or tuple)
// ------------------------------------------------------------
type ArrayElement<T> = T extends (infer E)[] ? E : never;
// ------------------------------------------------------------
// 4️⃣ Extract the props type of a React component
// ------------------------------------------------------------
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;
📚 Practical Example
// A simple async function
async function fetchUser(id: number): Promise<User> {
/* … */
}
// Use the helpers defined above
type FetchUserParams = Parameters<typeof fetchUser>; // => [number]
type FetchUserReturn = Return<typeof fetchUser>; // => Promise<User>
What the helpers do
| Helper | Input | Extracted type |
|---|---|---|
Parameters<T> | (...args: infer P) => any | P – a tuple of the function’s parameters |
Return<T> | (...args: any[]) => infer R | R – the function’s return type |
ArrayElement<T> | (infer E)[] | E – the type of each array element |
ComponentProps<T> | React.ComponentType<infer P> | P – the component’s props type |
These utilities are the building blocks for many advanced type‑level patterns in TypeScript, such as creating higher‑order functions, typed wrappers, or generic component libraries.
Mapped Types
Transform Object Types
// Make all properties optional
type Partial<T> = {
[P in keyof T]?: T[P];
};
// Make all properties required
type Required<T> = {
[P in keyof T]-?: T[P];
};
// Make all properties readonly
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// Pick specific properties
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Advanced Mapped Types
// Create getters for all properties
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person {
name: string;
age: number;
}
/* Example */
type PersonGetters = Getters<Person>;
/* Result:
{
getName: () => string;
getAge: () => number;
}
*/
// Filter properties by their value type
type FilterByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
id: number;
name: string;
active: boolean;
count: number;
}
/* Example */
type NumberProps = FilterByType<Mixed, number>;
/* Result:
{
id: number;
count: number;
}
*/
Note: By combining template‑literal types with mapped types you can build powerful type transformations—perfect for generating strongly‑typed API clients, form builders, and other meta‑programming scenarios.
Type Guards and Narrowing
Custom Type Guards
// Type predicate function
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"email" in value &&
typeof (value as User).id === "number" &&
typeof (value as User).email === "string"
);
}
// Usage
function processData(data: unknown) {
if (isUser(data)) {
// TypeScript knows `data` is `User` here
console.log(`User ${data.name} (${data.id})`);
} else {
console.warn("Not a user");
}
}
Discriminated Unions with Type Guards
interface SuccessResponse {
status: 'success';
data: User;
}
interface ErrorResponse {
status: 'error';
message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function handleResponse(response: ApiResponse) {
if (response.status === 'success') {
// TypeScript narrows to SuccessResponse
console.log(response.data.name);
} else {
// TypeScript narrows to ErrorResponse
console.log(response.message);
}
}
Assertion Functions
/**
* Asserts that the supplied value is a `User`.
* Throws an error if the check fails, allowing TypeScript to narrow the type.
*/
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new Error('Value is not a User');
}
}
/** Example usage */
function processUserData(data: unknown) {
// Narrow `data` to `User` after the assertion
assertIsUser(data);
console.log(data.email); // `data` is now known to be a `User`
}
/**
* Asserts that a value is neither `null` nor `undefined`.
*
* @param value The value to check.
* @param message Optional custom error message.
*
* @throws Error if `value` is `null` or `undefined`.
*/
function assertDefined<T>(
value: T | null | undefined,
message?: string
): asserts value is T {
if (value === null || value === undefined) {
throw new Error(message ?? 'Value is not defined');
}
}
Template Literal Types
No code example provided in the original segment.
Building Type‑Safe APIs
// ------------------------------------------------------------
// 1️⃣ Event handler types
// ------------------------------------------------------------
// Allowed event names
type EventName = 'click' | 'focus' | 'blur';
// Utility type that builds the handler name (e.g. "onClick")
type EventHandler<E extends EventName> = `on${Capitalize<E>}`;
// Example usage
type ClickHandler = EventHandler<'click'>; // "onClick"
type FocusHandler = EventHandler<'focus'>; // "onFocus"
type BlurHandler = EventHandler<'blur'>; // "onBlur"
// ------------------------------------------------------------
// 2️⃣ Route parameters extraction
// ------------------------------------------------------------
// Route patterns
type Route =
| '/users/:id'
| '/posts/:postId/comments/:commentId';
/**
* Recursively extracts all `:param` placeholders from a route string.
*
* @template T - The route string to analyse.
* @returns A union of the extracted parameter names.
*/
type ExtractParams<T extends string> =
// Match a segment that contains a parameter followed by more segments
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
// Match the final segment that contains a parameter
: T extends `${infer _Start}:${infer Param}`
? Param
// No more parameters
: never;
// Example extractions
type UserRouteParams = ExtractParams<'/users/:id'>; // "id"
type CommentRouteParams = ExtractParams<'/posts/:postId/comments/:commentId'>;
// → "postId" | "commentId"
The snippet above demonstrates how to:
- Generate type‑safe event‑handler names using template literal types and
Capitalize. - Extract route parameters from a URL pattern, yielding a union of the placeholder names.
Type‑Safe CSS Properties
type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;
interface Spacing {
margin: CSSValue;
padding: CSSValue;
}
const spacing: Spacing = {
margin: '16px', // Valid
padding: '1.5rem', // Valid
// margin: '16', // Error: not a valid CSSValue
};
Utility Types in Practice
(Section retained; no specific code was provided.)
Building a Form Library
Below is a small, type‑safe form‑configuration helper. It infers the appropriate input type from the shape of a data model and lets you add labels, validation, and required flags.
// ------------------------------------------------------------
// 1️⃣ Generic field‑configuration type
// ------------------------------------------------------------
type FieldConfig<T> = {
[K in keyof T]: {
/** Input type inferred from the property type */
type:
T[K] extends string
? 'text' | 'email' | 'password'
: T[K] extends number
? 'number'
: T[K] extends boolean
? 'checkbox'
: 'text';
/** Human‑readable label */
label: string;
/** Optional “required” flag */
required?: boolean;
/** Optional validator – returns an error string or `undefined` */
validate?: (value: T[K]) => string | undefined;
};
};
// ------------------------------------------------------------
// 2️⃣ Example data model
// ------------------------------------------------------------
interface UserForm {
name: string;
email: string;
age: number;
newsletter: boolean;
}
// ------------------------------------------------------------
// 3️⃣ Concrete configuration for `UserForm`
// ------------------------------------------------------------
const userFormConfig: FieldConfig<UserForm> = {
name: {
type: 'text',
label: 'Full Name',
required: true,
validate: value =>
}
}