TypeScript Utility Types
TypeScript's built-in utility types and how to build custom ones — Partial/Required, Pick/Omit, Readonly/Record, Extract/Exclude, NonNullable/Awaited, ReturnType/Parameters/ConstructorParameters/InstanceType, key remapping with `as` in mapped types, and hand-rolled custom mapped types. Use when picking the right built-in utility type instead of hand-writing one, extracting a function's parameter or return type, filtering a union with Extract/Exclude, or remapping object keys.
npx skills add github:pantheon-org/tekhne --skill development--typescript-advanced--utility-typesThe standard library of type transformations: which built-in utility type fits a given shape change, and how to build a custom one when none does.
Mindset
Reach for a built-in utility type before writing a custom one — Partial<T>, Pick<T, K>, ReturnType<typeof fn> and their relatives cover the overwhelming majority of shape transformations, and a hand-rolled equivalent is one more thing to keep in sync with the type it derives from. When a built-in genuinely doesn’t fit (a transform needs to recurse into nested objects, or apply a key-by-key rename), build the custom version as a mapped type over the real source type rather than a parallel interface maintained by hand. Combine utilities rather than inventing a new named type for every combination: Partial<Pick<T, K>> is more honest about what it does than a bespoke PartialSubset<T, K> that hides the same composition behind a name.
When to Apply
Use this skill when:
- Making some or all properties of a type optional or required (
Partial,Required) - Selecting or excluding specific properties (
Pick,Omit) - Making a type read-only or building a lookup type (
Readonly,Record) - Filtering members out of or into a union (
Extract,Exclude) - Stripping
null/undefinedor unwrapping aPromise(NonNullable,Awaited) - Extracting a function’s return type, parameters, constructor parameters, or instance type
- Remapping object keys in a mapped type with an
asclause - Building a custom mapped type because no built-in utility fits
Use When
- “Is there a built-in utility type for this, or do I need to write my own?”
- “How do I extract just the parameter types from this function signature?”
- “How do I make every property in this type optional except one?”
- “How do I rename or filter keys while mapping over a type?”
Scope
In Scope
- Every TypeScript built-in utility type (
Partial,Required,Pick,Omit,Readonly,Record,Extract,Exclude,NonNullable,Awaited,ReturnType,Parameters,ConstructorParameters,InstanceType,ThisParameterType,OmitThisParameter). - Key remapping in mapped types (
asclauses, key filtering). - Custom mapped types for transforms no built-in covers.
Out of Scope
- The general mapped-type and conditional-type mechanics these utilities are built from — see the sibling
typescript-type-systemskill. - Deep/recursive variants (
DeepReadonly,DeepPartial) that recurse into nested objects — seedeep-readonly.mdin the siblingtypescript-design-patternsskill. - Runtime type guards — see the sibling
typescript-type-guardsskill.
When NOT to Use
Do not use this skill to look up how mapped types or conditional types work mechanically — it assumes that grounding and focuses on the ready-made utilities built from it. For a recursive deep-readonly/deep-partial that walks nested objects, use typescript-design-patterns’ deep-readonly.md instead — the utilities here are shallow, one level deep, matching TypeScript’s actual built-ins.
Quick Commands
Type Check
npx tsc --noEmit
Anti-Patterns
NEVER hand-write a type that a built-in utility already provides
WHY: a hand-written equivalent of Partial<T> or Pick<T, K> doesn’t stay in sync when the source type changes, while the built-in always reflects the current shape.
BAD:
interface UserUpdate {
name?: string;
email?: string;
age?: number;
} // duplicates User, optional — drifts if User changes
GOOD:
type UserUpdate = Partial<User>;
NEVER use ReturnType<fn> when you mean ReturnType<typeof fn>
WHY: fn the value and typeof fn the type are different things in a type position — passing the value’s name directly is a type error, not a silent no-op, but it’s a common enough slip to call out explicitly.
BAD:
function getUser() { return { id: 1 }; }
type User = ReturnType<getUser>; // error: 'getUser' refers to a value
GOOD:
type User = ReturnType<typeof getUser>;
NEVER invent a bespoke named type for a simple composition of built-ins
WHY: a name like PartialSubset<T, K> hides Partial<Pick<T, K>> behind an extra layer a reader has to look up, for no gain over writing the composition directly.
BAD:
type PartialSubset<T, K extends keyof T> = Partial<Pick<T, K>>;
// used once, adds an indirection with no new meaning
GOOD:
type UserPatch = Partial<Pick<User, "name" | "email">>;
References
| File | Covers |
|---|---|
references/partial-required.md |
Partial<T>, Required<T> |
references/pick-omit.md |
Pick<T, K>, Omit<T, K> |
references/readonly-record.md |
Readonly<T>, Record<K, T> |
references/extract-exclude.md |
Extract<T, U>, Exclude<T, U> |
references/nonnullable-awaited.md |
NonNullable<T>, Awaited<T> |
references/returntype-parameters.md |
ReturnType, Parameters, ConstructorParameters, InstanceType, ThisParameterType, OmitThisParameter |
references/key-remapping.md |
Key remapping with as in mapped types |
references/custom-mapped-types.md |
Building a custom mapped type when no built-in fits |
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
No eval scenarios available.
Custom Mapped Types
Creating reusable type transformations with mapped types.
Basic Mapped Type Pattern
type Mapped<T> = {
[K in keyof T]: T[K]
};
// With transformation
type Nullable<T> = {
[K in keyof T]: T[K] | null
};
type Optional<T> = {
[K in keyof T]?: T[K]
};
Modifier Flags
Adding/Removing readonly
// Add readonly
type Immutable<T> = {
+readonly [K in keyof T]: T[K]
};
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K]
};
Adding/Removing optional
// Make all optional
type AllOptional<T> = {
[K in keyof T]?: T[K]
};
// Make all required
type AllRequired<T> = {
[K in keyof T]-?: T[K]
};
Advanced Mapped Types
Getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
type User = { name: string; age: number };
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
Setters
type Setters<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
};
type UserSetters = Setters<User>;
// { setName: (value: string) => void; setAge: (value: number) => void }
Combined Accessors
type Accessors<T> = Getters<T> & Setters<T>;
class UserClass implements Accessors<User> {
private data: User = { name: '', age: 0 };
getName() { return this.data.name; }
setName(value: string) { this.data.name = value; }
getAge() { return this.data.age; }
setAge(value: number) { this.data.age = value; }
}
Promisify
Convert sync functions to async.
type Promisify<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T[K]
};
interface SyncAPI {
getUser: (id: string) => User;
createUser: (data: UserInput) => User;
}
type AsyncAPI = Promisify<SyncAPI>;
// {
// getUser: (id: string) => Promise<User>;
// createUser: (data: UserInput) => Promise<User>;
// }
DeepReadonly & DeepPartial
Recursive type transformations.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K]
};
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]>
: T[K]
};
type Config = {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
};
type ImmutableConfig = DeepReadonly<Config>;
// All properties readonly, including nested
Filtering Properties
By Type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
};
type OmitByType<T, U> = {
[K in keyof T as T[K] extends U ? never : K]: T[K]
};
interface Mixed {
id: string;
name: string;
age: number;
active: boolean;
}
type StringProps = PickByType<Mixed, string>;
// { id: string; name: string }
type NotStrings = OmitByType<Mixed, string>;
// { age: number; active: boolean }
By Key Pattern
type PickByKeyPrefix<T, Prefix extends string> = {
[K in keyof T as K extends `${Prefix}${string}` ? K : never]: T[K]
};
interface User {
id: string;
name: string;
isActive: boolean;
isAdmin: boolean;
createdAt: Date;
}
type BooleanFlags = PickByKeyPrefix<User, 'is'>;
// { isActive: boolean; isAdmin: boolean }
Practical Patterns
API Response Wrapper
type APIResponse<T> = {
[K in keyof T]: {
data: T[K];
loading: boolean;
error: Error | null;
}
};
interface Data {
user: User;
posts: Post[];
}
type WrappedData = APIResponse<Data>;
// {
// user: { data: User; loading: boolean; error: Error | null };
// posts: { data: Post[]; loading: boolean; error: Error | null };
// }
Event Handlers
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
value: T[K]
) => void
};
interface FormData {
username: string;
email: string;
age: number;
}
type FormHandlers = EventHandlers<FormData>;
// {
// onUsernameChange: (value: string) => void;
// onEmailChange: (value: string) => void;
// onAgeChange: (value: number) => void;
// }
Validation Schema
type ValidationSchema<T> = {
[K in keyof T]: {
required: boolean;
validator: (value: T[K]) => boolean;
message: string;
}
};
const userSchema: ValidationSchema<User> = {
name: {
required: true,
validator: (v) => v.length > 0,
message: 'Name is required'
},
age: {
required: true,
validator: (v) => v > 0,
message: 'Age must be positive'
}
};
Boxed Types
Wrap primitives in objects.
type Boxed<T> = {
[K in keyof T]: { value: T[K] }
};
type Unbox<T> = {
[K in keyof T]: T[K] extends { value: infer V } ? V : T[K]
};
interface Config {
port: number;
host: string;
}
type BoxedConfig = Boxed<Config>;
// { port: { value: number }; host: { value: string } }
type UnboxedConfig = Unbox<BoxedConfig>;
// { port: number; host: string }
Best Practices
- Name mapped types clearly: Use descriptive names like
Promisify,Getters - Document transformations: Explain what the mapped type does
- Limit recursion depth: Deep recursion can impact performance
- Use with const assertions: Preserve literal types
- Test with complex types: Ensure mapped types work with edge cases
Common Patterns
State Management
type StateSlice<T> = {
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
} & {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
} & {
[K in keyof T as `reset${Capitalize<string & K>}`]: () => void
};
Dependency Injection
type Injectable<T> = {
[K in keyof T]: T[K] extends (...args: infer A) => infer R
? (...args: [...A, dependencies: Record<string, any>]) => R
: T[K]
};
Anti-Patterns
❌ Overly complex mapped types
type Bad<T> = {
[K in keyof T as K extends string
? T[K] extends number
? `num_${K}`
: T[K] extends string
? `str_${K}`
: K
: K]: T[K]
};
✅ Split into smaller utilities
type PrefixNumbers<T> = { [K in keyof T as /* ... */]: T[K] };
type PrefixStrings<T> = { [K in keyof T as /* ... */]: T[K] };
type Good<T> = PrefixNumbers<T> & PrefixStrings<T>;
Extract and Exclude Utility Types
Exclude<T, U>
Removes types from T that are assignable to U.
Basic Usage
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type PrimaryColors = Exclude<AllColors, 'yellow'>;
// Type: 'red' | 'green' | 'blue'
type NonBlueColors = Exclude<AllColors, 'blue' | 'green'>;
// Type: 'red' | 'yellow'
Implementation
type Exclude<T, U> = T extends U ? never : T;
How it works: Distributive conditional type - checks each member of T union.
Use Cases
Filter Union Types
type Status = 'pending' | 'approved' | 'rejected' | 'cancelled';
type ActiveStatus = Exclude<Status, 'cancelled'>;
// 'pending' | 'approved' | 'rejected'
function processActiveOrder(status: ActiveStatus) {
// status cannot be 'cancelled'
}
Remove null/undefined
type MaybeString = string | null | undefined;
type DefiniteString = Exclude<MaybeString, null | undefined>;
// Type: string
// Note: NonNullable does the same thing
type DefiniteString2 = NonNullable<MaybeString>;
// Type: string
Filter by Type
type Mixed = string | number | boolean | null | undefined;
type OnlyPrimitives = Exclude<Mixed, null | undefined>;
// string | number | boolean
type OnlyStringsAndNumbers = Exclude<Mixed, boolean | null | undefined>;
// string | number
Remove Function Types
type Value = string | number | (() => void) | (() => string);
type NonFunction = Exclude<Value, Function>;
// string | number
Extract<T, U>
Extracts from T only types that are assignable to U (opposite of Exclude).
Basic Usage
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type WarmColors = Extract<AllColors, 'red' | 'yellow' | 'orange'>;
// Type: 'red' | 'yellow' (orange doesn't exist in AllColors)
Implementation
type Extract<T, U> = T extends U ? T : never;
Use Cases
Filter to Specific Types
type Mixed = string | number | boolean | null;
type StringOrNumber = Extract<Mixed, string | number>;
// string | number
type OnlyString = Extract<Mixed, string>;
// string
Extract Function Types
type Value = string | number | (() => void) | (() => string);
type OnlyFunctions = Extract<Value, Function>;
// (() => void) | (() => string)
Extract Object Types
type Mixed = string | number | { name: string } | { id: number };
type OnlyObjects = Extract<Mixed, object>;
// { name: string } | { id: number }
Pattern Matching with Strings
type Events = 'onClick' | 'onHover' | 'onFocus' | 'handleClick' | 'handleSubmit';
// Extract events starting with 'on'
type OnEvents = Extract<Events, `on${string}`>;
// 'onClick' | 'onHover' | 'onFocus'
type HandleEvents = Extract<Events, `handle${string}`>;
// 'handleClick' | 'handleSubmit'
Combining Extract and Exclude
Complex Filtering
type ApiResponse =
| { status: 'success'; data: any }
| { status: 'error'; error: string }
| { status: 'loading' }
| { status: 'idle' };
// Extract responses with data/error
type ResponseWithPayload = Extract<ApiResponse, { data: any } | { error: any }>;
// { status: 'success'; data: any } | { status: 'error'; error: string }
// Exclude loading states
type CompletedResponse = Exclude<ApiResponse, { status: 'loading' | 'idle' }>;
// { status: 'success'; data: any } | { status: 'error'; error: string }
Type-Safe Event Handlers
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
scroll: Event;
custom: CustomEvent;
};
type EventName = keyof EventMap;
// Extract only standard DOM events
type StandardEvents = Extract<EventName, 'click' | 'keydown' | 'scroll'>;
// 'click' | 'keydown' | 'scroll'
// Exclude standard events (get custom ones)
type CustomEvents = Exclude<EventName, 'click' | 'keydown' | 'scroll'>;
// 'custom'
NonNullable<T>
Built-in utility using Exclude:
type NonNullable<T> = Exclude<T, null | undefined>;
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
Practical Patterns
Filter Props by Value Type
type User = {
id: string;
name: string;
age: number;
isActive: boolean;
email: string;
};
// Get keys with string values
type StringKeys<T> = {
[K in keyof T]: T[K] extends string ? K : never;
}[keyof T];
type UserStringKeys = StringKeys<User>;
// 'id' | 'name' | 'email'
// Extract string properties
type StringProps = Pick<User, Extract<keyof User, StringKeys<User>>>;
Exclude Internal Properties
interface Component {
_id: string;
_internal: boolean;
name: string;
value: string;
}
type PublicKeys<T> = Exclude<keyof T, `_${string}`>;
type PublicComponent = Pick<Component, PublicKeys<Component>>;
// {
// name: string;
// value: string;
// }
Type-Safe Union Filtering
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; size: number }
| { kind: 'rectangle'; width: number; height: number };
type RoundShapes = Extract<Shape, { kind: 'circle' }>;
// { kind: 'circle'; radius: number }
type NonCircleShapes = Exclude<Shape, { kind: 'circle' }>;
// { kind: 'square'; size: number } | { kind: 'rectangle'; width: number; height: number }
Best Practices
- Use Exclude for denylists - Remove unwanted types from unions
- Use Extract for allowlists - Keep only wanted types from unions
- Combine with conditional types - Build complex type transformations
- Use NonNullable for null safety - Remove null/undefined explicitly
- Extract with template literals - Pattern match string literal types
Common Patterns
API Status Types
type ApiStatus = 'idle' | 'loading' | 'success' | 'error';
type ActiveStatus = Exclude<ApiStatus, 'idle'>;
// 'loading' | 'success' | 'error'
type CompletedStatus = Extract<ApiStatus, 'success' | 'error'>;
// 'success' | 'error'
Permission Filtering
type Permission = 'read' | 'write' | 'delete' | 'admin';
type BasicPermissions = Exclude<Permission, 'admin'>;
// 'read' | 'write' | 'delete'
type ModifyPermissions = Extract<Permission, 'write' | 'delete'>;
// 'write' | 'delete'
Common Pitfalls
- Excluding non-existent types - No error, returns original type
- Extract with no matches - Returns
never - Forgetting distributive behavior - Works on union members, not object properties
- Using on non-union types - Exclude/Extract are for unions, not object shapes
Key Remapping in Mapped Types
Advanced property key transformations using as clause.
Basic Key Remapping
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
type User = { name: string; age: number };
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
Filtering Properties
Remove Keys
type RemoveKind<T> = {
[K in keyof T as Exclude<K, 'kind'>]: T[K]
};
type WithKind = { kind: 'user'; name: string; age: number };
type WithoutKind = RemoveKind<WithKind>;
// { name: string; age: number }
Filter by Type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K]
};
interface Mixed {
id: string;
name: string;
count: number;
active: boolean;
}
type OnlyStrings = PickByType<Mixed, string>;
// { id: string; name: string }
Prefix and Suffix
type AddPrefix<T, Prefix extends string> = {
[K in keyof T as `${Prefix}${string & K}`]: T[K]
};
type AddSuffix<T, Suffix extends string> = {
[K in keyof T as `${string & K}${Suffix}`]: T[K]
};
type Stats = { views: number; likes: number };
type PrefixedStats = AddPrefix<Stats, 'total'>;
// { totalviews: number; totallikes: number }
type SuffixedStats = AddSuffix<Stats, 'Count'>;
// { viewsCount: number; likesCount: number }
Case Transformations
type Capitalize<S extends string> =
S extends `${infer F}${infer R}` ? `${Uppercase<F>}${R}` : S;
type CamelToSnake<S extends string> =
S extends `${infer T}${infer U}`
? U extends Uncapitalize<U>
? `${Lowercase<T>}${CamelToSnake<U>}`
: `${Lowercase<T>}_${CamelToSnake<U>}`
: S;
type SnakeToCamel<S extends string> =
S extends `${infer T}_${infer U}`
? `${T}${Capitalize<SnakeToCamel<U>>}`
: S;
type ToSnakeCase<T> = {
[K in keyof T as CamelToSnake<string & K>]: T[K]
};
type ToCamelCase<T> = {
[K in keyof T as SnakeToCamel<string & K>]: T[K]
};
type JS = { userId: string; firstName: string };
type DB = ToSnakeCase<JS>;
// { user_id: string; first_name: string }
Conditional Remapping
type RenameKey<T, Old extends keyof T, New extends string> = {
[K in keyof T as K extends Old ? New : K]: T[K]
};
type User = { id: string; name: string };
type UserWithUUID = RenameKey<User, 'id', 'uuid'>;
// { uuid: string; name: string }
Event Name Transformations
type EventHandlers<T> = {
[K in keyof T as `on${Capitalize<string & K>}Change`]: (
value: T[K],
oldValue: T[K]
) => void
};
interface FormState {
username: string;
email: string;
age: number;
}
type FormHandlers = EventHandlers<FormState>;
// {
// onUsernameChange: (value: string, oldValue: string) => void;
// onEmailChange: (value: string, oldValue: string) => void;
// onAgeChange: (value: number, oldValue: number) => void;
// }
Method Generation
CRUD Operations
type CRUDOperations<T, IdKey extends keyof T> = {
[K in keyof T as `create${Capitalize<string & K>}`]: (
value: T[K]
) => Promise<T>
} & {
[K in keyof T as `read${Capitalize<string & K>}`]: (
id: T[IdKey]
) => Promise<T>
} & {
[K in keyof T as `update${Capitalize<string & K>}`]: (
id: T[IdKey],
value: Partial<T[K]>
) => Promise<T>
} & {
[K in keyof T as `delete${Capitalize<string & K>}`]: (
id: T[IdKey]
) => Promise<void>
};
Validators
type Validators<T> = {
[K in keyof T as `validate${Capitalize<string & K>}`]: (
value: T[K]
) => boolean
} & {
[K in keyof T as `is${Capitalize<string & K>}Valid`]: boolean
};
interface UserInput {
email: string;
password: string;
}
type UserValidators = Validators<UserInput>;
// {
// validateEmail: (value: string) => boolean;
// isEmailValid: boolean;
// validatePassword: (value: string) => boolean;
// isPasswordValid: boolean;
// }
Practical Patterns
API Route Types
type APIRoutes<T> = {
[K in keyof T as `/api/${string & K}`]: T[K]
};
interface Endpoints {
users: User[];
posts: Post[];
comments: Comment[];
}
type Routes = APIRoutes<Endpoints>;
// {
// '/api/users': User[];
// '/api/posts': Post[];
// '/api/comments': Comment[];
// }
Redux Actions
type Actions<T> = {
[K in keyof T as `SET_${Uppercase<string & K>}`]: {
type: `SET_${Uppercase<string & K>}`;
payload: T[K];
}
}[keyof T];
interface State {
user: User;
theme: 'light' | 'dark';
loading: boolean;
}
type StateActions = Actions<State>;
// { type: 'SET_USER'; payload: User }
// | { type: 'SET_THEME'; payload: 'light' | 'dark' }
// | { type: 'SET_LOADING'; payload: boolean }
Database Column Mapping
type DBColumns<T> = {
[K in keyof T as `${string & K}_column`]: {
name: string;
type: T[K] extends string ? 'TEXT'
: T[K] extends number ? 'INTEGER'
: T[K] extends boolean ? 'BOOLEAN'
: 'JSON';
nullable: boolean;
}
};
Complex Key Transformations
Extract Prefix
type ExtractPrefix<S extends string, Prefix extends string> =
S extends `${Prefix}${infer Rest}` ? Rest : never;
type RemovePrefix<T, Prefix extends string> = {
[K in keyof T as ExtractPrefix<string & K, Prefix>]: T[K]
};
interface PrefixedAPI {
api_getUser: () => User;
api_createUser: (data: UserInput) => User;
other: string;
}
type CleanAPI = RemovePrefix<PrefixedAPI, 'api_'>;
// { getUser: () => User; createUser: (data: UserInput) => User }
Nested Key Paths
type PathKeys<T, Prefix extends string = ''> = {
[K in keyof T]: K extends string
? T[K] extends object
? `${Prefix}${K}` | PathKeys<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`
: never
}[keyof T];
type User = {
name: string;
address: {
street: string;
city: string;
};
};
type UserPaths = PathKeys<User>;
// 'name' | 'address' | 'address.street' | 'address.city'
Best Practices
- Use
string &for string keys: Ensures key is string type - Leverage template literals: Build complex key patterns
- Combine with conditional types: Filter and transform simultaneously
- Document naming conventions: Explain key transformation rules
- Test with various inputs: Verify remapping works with edge cases
Common Use Cases
React Props
type EventProps<T> = {
[K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void
};
interface InputState {
value: string;
focused: boolean;
}
type InputProps = InputState & EventProps<InputState>;
// {
// value: string;
// focused: boolean;
// onValue: (value: string) => void;
// onFocused: (value: boolean) => void;
// }
GraphQL Field Selection
type SelectFields<T, Selected extends keyof T> = {
[K in Selected as `select${Capitalize<string & K>}`]: boolean
};
Anti-Patterns
❌ Unclear naming transformations
type Bad<T> = {
[K in keyof T as `x${string & K}y`]: T[K]
}; // What does 'x' and 'y' mean?
✅ Clear semantic naming
type Good<T> = {
[K in keyof T as `fetch${Capitalize<string & K>}`]: () => Promise<T[K]>
};
❌ Over-complex key logic
type Bad<T> = {
[K in keyof T as K extends `${infer A}_${infer B}_${infer C}`
? /* complex nested logic */
: K]: T[K]
};
✅ Break into helper types
type ParseKey<K> = /* ... */;
type Good<T> = {
[K in keyof T as ParseKey<K>]: T[K]
};
NonNullable, Awaited & Other Utility Types
Utilities for handling nullable types and promises.
NonNullable<T>
Removes null and undefined from a type.
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
type MaybeUser = { name: string } | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // { name: string }
Practical Usage
function processValue<T>(value: T): NonNullable<T> {
if (value === null || value === undefined) {
throw new Error('Value cannot be null or undefined');
}
return value;
}
// Filter nulls from arrays
function filterNulls<T>(arr: (T | null | undefined)[]): NonNullable<T>[] {
return arr.filter((item): item is NonNullable<T> =>
item !== null && item !== undefined
);
}
Awaited<T>
Recursively unwraps Promise types.
type P1 = Promise<string>;
type R1 = Awaited<P1>; // string
type P2 = Promise<Promise<number>>;
type R2 = Awaited<P2>; // number (recursive unwrap)
type P3 = Promise<{ data: Promise<User> }>;
type R3 = Awaited<P3>; // { data: Promise<User> } (shallow unwrap)
With Async Functions
async function fetchUser(): Promise<{ id: string; name: string }> {
// Implementation
return { id: '1', name: 'John' };
}
type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: string; name: string }
Handling Promise Arrays
type PromiseArray = Promise<User>[];
type ResolvedArray = Awaited<typeof Promise.all<PromiseArray>>;
// User[]
async function fetchAll<T>(
promises: Promise<T>[]
): Promise<Awaited<T>[]> {
return Promise.all(promises);
}
Uppercase, Lowercase, Capitalize, Uncapitalize
String manipulation utilities.
type Greeting = 'hello world';
type Loud = Uppercase<Greeting>; // 'HELLO WORLD'
type Quiet = Lowercase<Greeting>; // 'hello world'
type Proper = Capitalize<Greeting>; // 'Hello world'
type Casual = Uncapitalize<'Hello'>; // 'hello'
Template Literal Combinations
type HTTPMethod = 'get' | 'post' | 'put' | 'delete';
type UpperMethod = Uppercase<HTTPMethod>;
// 'GET' | 'POST' | 'PUT' | 'DELETE'
type EventName = 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur'
Practical Patterns
Safe Async Operations
type AsyncResult<T> = Promise<
| { success: true; data: T }
| { success: false; error: Error }
>;
async function safeAsync<T>(
promise: Promise<T>
): AsyncResult<T> {
try {
const data = await promise;
return { success: true, data };
} catch (error) {
return { success: false, error: error as Error };
}
}
// Extract successful data type
type SuccessData<T> = T extends { success: true; data: infer D }
? D
: never;
type UserData = Awaited<ReturnType<typeof fetchUser>>;
Non-Nullable Object Properties
type User = {
id: string;
name: string | null;
email?: string;
age: number | undefined;
};
type RequiredUser = {
[K in keyof User]-?: NonNullable<User[K]>
};
// { id: string; name: string; email: string; age: number }
Promise Resolution Types
interface APIEndpoints {
getUser: () => Promise<User>;
getPosts: () => Promise<Post[]>;
getComments: (postId: string) => Promise<Comment[]>;
}
type ResolvedAPI = {
[K in keyof APIEndpoints]: Awaited<ReturnType<APIEndpoints[K]>>
};
// { getUser: User; getPosts: Post[]; getComments: Comment[] }
Event Name Transformations
type DOMEvent = 'click' | 'mouseenter' | 'focus';
type EventHandler<E extends string> = {
[K in E as `on${Capitalize<K>}`]: (event: Event) => void
};
type Handlers = EventHandler<DOMEvent>;
// {
// onClick: (event: Event) => void;
// onMouseenter: (event: Event) => void;
// onFocus: (event: Event) => void;
// }
Best Practices
- Use NonNullable with type guards: Narrow types safely
- Awaited for async return types: Extract promise values
- String utilities for type safety: Generate consistent naming
- Combine with conditional types: Build flexible utilities
- Document unwrapping behavior: Clarify recursive vs shallow
Common Use Cases
Form Validation
type FormData = {
username: string | null;
email: string | null;
age: number | null;
};
type ValidatedForm = {
[K in keyof FormData]: NonNullable<FormData[K]>
};
function validateForm(data: FormData): ValidatedForm {
const validated: Partial<ValidatedForm> = {};
for (const key in data) {
const value = data[key as keyof FormData];
if (value === null) {
throw new Error(`${key} is required`);
}
(validated as any)[key] = value;
}
return validated as ValidatedForm;
}
Async Data Fetching
type DataFetcher = {
user: () => Promise<User>;
posts: (userId: string) => Promise<Post[]>;
comments: (postId: string) => Promise<Comment[]>;
};
type FetcherResults = {
[K in keyof DataFetcher]: Awaited<ReturnType<DataFetcher[K]>>
};
// { user: User; posts: Post[]; comments: Comment[] }
Anti-Patterns
❌ Manual null checks without types
function bad(value: string | null) {
if (value !== null) {
// TypeScript doesn't know value is string here
return value;
}
}
✅ Type guards with NonNullable
function good<T>(value: T): NonNullable<T> {
if (value === null || value === undefined) {
throw new Error('Null value');
}
return value; // TypeScript knows this is NonNullable<T>
}
❌ Ignoring nested promises
type Bad = ReturnType<typeof async1>; // Promise<Promise<T>>
✅ Use Awaited
type Good = Awaited<ReturnType<typeof async1>>; // T
Partial and Required Utility Types
Partial<T>
Makes all properties in T optional.
Basic Usage
interface User {
id: string;
name: string;
email: string;
age: number;
}
type PartialUser = Partial<User>;
// {
// id?: string;
// name?: string;
// email?: string;
// age?: number;
// }
function updateUser(id: string, updates: Partial<User>) {
// Apply only the provided fields
}
updateUser('123', { name: 'John' }); // ✅ Only name provided
updateUser('123', { age: 30, email: 'john@example.com' }); // ✅
Implementation
type Partial<T> = {
[P in keyof T]?: T[P];
};
Use Cases
API Updates (PATCH)
async function patchUser(id: string, updates: Partial<User>) {
return fetch(`/api/users/${id}`, {
method: 'PATCH',
body: JSON.stringify(updates)
});
}
Form State
interface FormData {
username: string;
password: string;
email: string;
}
function FormComponent() {
const [formData, setFormData] = useState<Partial<FormData>>({});
const updateField = (field: keyof FormData, value: string) => {
setFormData(prev => ({ ...prev, [field]: value }));
};
}
Default Configuration
interface Config {
host: string;
port: number;
timeout: number;
retries: number;
}
const defaultConfig: Config = {
host: 'localhost',
port: 3000,
timeout: 5000,
retries: 3
};
function createConfig(overrides: Partial<Config>): Config {
return { ...defaultConfig, ...overrides };
}
const config = createConfig({ port: 8080 }); // ✅ Only override port
Required<T>
Makes all properties in T required.
Basic Usage
interface User {
id?: string;
name?: string;
email?: string;
}
type CompleteUser = Required<User>;
// {
// id: string;
// name: string;
// email: string;
// }
function saveUser(user: Required<User>) {
// All fields guaranteed to exist
console.log(user.id.toUpperCase()); // ✅ No undefined check needed
}
Implementation
type Required<T> = {
[P in keyof T]-?: T[P]; // -? removes optional modifier
};
Use Cases
Database Operations
interface DraftPost {
title?: string;
content?: string;
authorId?: string;
publishedAt?: Date;
}
// Database requires all fields
function insertPost(post: Required<DraftPost>) {
db.posts.insert(post);
}
// Validation before saving
function validateAndSave(draft: DraftPost) {
if (isComplete(draft)) {
insertPost(draft as Required<DraftPost>);
}
}
function isComplete(draft: DraftPost): draft is Required<DraftPost> {
return !!(draft.title && draft.content && draft.authorId);
}
API Response Validation
interface ApiUser {
id?: string;
name?: string;
email?: string;
}
function validateUser(user: ApiUser): Required<ApiUser> {
if (!user.id || !user.name || !user.email) {
throw new Error('Invalid user data');
}
return user as Required<ApiUser>;
}
Combining Partial and Required
Selective Optionality
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: string;
name: string;
email: string;
age: number;
}
// Make only age optional
type UserWithOptionalAge = PartialBy<User, 'age'>;
// {
// id: string;
// name: string;
// email: string;
// age?: number;
// }
Selective Requirements
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
interface DraftUser {
id?: string;
name?: string;
email?: string;
}
// Require only name and email
type MinimalUser = RequiredBy<DraftUser, 'name' | 'email'>;
// {
// id?: string;
// name: string;
// email: string;
// }
Deep Partial/Required
For nested objects:
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
type DeepRequired<T> = {
[P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
};
interface NestedConfig {
server: {
host: string;
port: number;
ssl: {
enabled: boolean;
cert: string;
};
};
}
type PartialConfig = DeepPartial<NestedConfig>;
// All nested properties become optional
const config: PartialConfig = {
server: {
port: 8080
// ssl not required, host not required
}
};
Best Practices
- Use Partial for updates - PATCH operations, form updates
- Use Required for validation - Ensure complete data before processing
- Combine with type guards - Validate before treating as Required
- Consider deep variants - For nested objects
- Document assumptions - Comment why fields are optional/required
Common Pitfalls
- Assuming Partial values exist - Always check before accessing
- Over-using Partial - Makes API unclear about required fields
- Type assertion without validation - Don’t cast to Required without checking
- Ignoring nested optionality - Use DeepPartial for nested objects
Pick and Omit Utility Types
Pick<T, K>
Creates a type by picking specific properties from T.
Basic Usage
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
// {
// id: string;
// name: string;
// email: string;
// }
function displayUser(user: PublicUser) {
// Only id, name, email accessible
console.log(user.name);
// console.log(user.password); // ❌ Error
}
Implementation
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Use Cases
API Response DTOs
interface DbUser {
id: string;
name: string;
email: string;
passwordHash: string;
salt: string;
createdAt: Date;
updatedAt: Date;
}
type UserResponse = Pick<DbUser, 'id' | 'name' | 'email' | 'createdAt'>;
function getUserById(id: string): UserResponse {
const dbUser = db.users.findById(id);
return {
id: dbUser.id,
name: dbUser.name,
email: dbUser.email,
createdAt: dbUser.createdAt
};
}
Form Fields
interface Product {
id: string;
name: string;
description: string;
price: number;
stock: number;
createdAt: Date;
}
type ProductFormData = Pick<Product, 'name' | 'description' | 'price' | 'stock'>;
function ProductForm() {
const [formData, setFormData] = useState<ProductFormData>({
name: '',
description: '',
price: 0,
stock: 0
});
}
Minimal Interface
interface TodoItem {
id: string;
title: string;
description: string;
completed: boolean;
createdAt: Date;
updatedAt: Date;
tags: string[];
}
type TodoSummary = Pick<TodoItem, 'id' | 'title' | 'completed'>;
function getTodoList(): TodoSummary[] {
// Return minimal data for list view
}
Omit<T, K>
Creates a type by omitting specific properties from T.
Basic Usage
interface User {
id: string;
name: string;
email: string;
password: string;
}
type UserWithoutPassword = Omit<User, 'password'>;
// {
// id: string;
// name: string;
// email: string;
// }
type PublicUser = Omit<User, 'password' | 'email'>;
// {
// id: string;
// name: string;
// }
Implementation
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Note: K is keyof any (not keyof T), allowing omitting keys that don’t exist.
Use Cases
Remove Sensitive Data
interface Employee {
id: string;
name: string;
email: string;
salary: number;
ssn: string;
}
type PublicEmployee = Omit<Employee, 'salary' | 'ssn'>;
function getEmployeeProfile(id: string): PublicEmployee {
const employee = db.employees.findById(id);
const { salary, ssn, ...public } = employee;
return public;
}
Exclude Auto-Generated Fields
interface DbRecord {
id: string;
createdAt: Date;
updatedAt: Date;
data: any;
}
type CreateRecordInput = Omit<DbRecord, 'id' | 'createdAt' | 'updatedAt'>;
async function createRecord(input: CreateRecordInput): Promise<DbRecord> {
return db.records.create({
...input,
id: generateId(),
createdAt: new Date(),
updatedAt: new Date()
});
}
Extend and Modify
interface BaseConfig {
host: string;
port: number;
timeout: number;
}
// Replace timeout with different type
interface ExtendedConfig extends Omit<BaseConfig, 'timeout'> {
timeout: {
connect: number;
read: number;
};
}
const config: ExtendedConfig = {
host: 'localhost',
port: 3000,
timeout: {
connect: 5000,
read: 30000
}
};
Pick vs Omit
Choose based on which is more concise:
interface LargeType {
a: string;
b: number;
c: boolean;
d: Date;
e: string[];
f: object;
}
// If you need most properties, use Omit
type MostProps = Omit<LargeType, 'f'>;
// If you need few properties, use Pick
type FewProps = Pick<LargeType, 'a' | 'b'>;
Combining Pick and Omit
PickAndModify
type ModifyProp<T, K extends keyof T, NewType> =
Omit<T, K> & Record<K, NewType>;
interface User {
id: string;
name: string;
age: number;
}
// Change age from number to string
type UserWithStringAge = ModifyProp<User, 'age', string>;
// {
// id: string;
// name: string;
// age: string;
// }
PickOptional
type PickOptional<T, K extends keyof T> =
Pick<T, K> & Partial<Omit<T, K>>;
interface Config {
host: string;
port: number;
ssl: boolean;
timeout: number;
}
// host and port required, others optional
type MinimalConfig = PickOptional<Config, 'host' | 'port'>;
// {
// host: string;
// port: number;
// ssl?: boolean;
// timeout?: number;
// }
Best Practices
- Use Pick for allowlists - Explicitly state what you want
- Use Omit for denylists - Remove what you don’t want
- Prefer Pick for public APIs - More explicit about exposed fields
- Use Omit for extending types - Remove fields before adding new ones
- Combine with other utilities - Partial, Required, Readonly
Common Patterns
API Layer Types
interface DbUser {
id: string;
username: string;
email: string;
passwordHash: string;
role: string;
}
// Public API response
type UserDto = Omit<DbUser, 'passwordHash'>;
// Create user input
type CreateUserDto = Omit<DbUser, 'id'>;
// Update user input
type UpdateUserDto = Partial<Omit<DbUser, 'id' | 'passwordHash'>>;
Form Handling
interface Article {
id: string;
title: string;
content: string;
authorId: string;
publishedAt: Date | null;
createdAt: Date;
}
type ArticleFormData = Pick<Article, 'title' | 'content'>;
type ArticleCreateData = Omit<Article, 'id' | 'createdAt'>;
Common Pitfalls
- Omitting non-existent keys - No error, but might indicate typo
- Using Pick with too many keys - Consider Omit instead
- Forgetting to update after schema changes - Can expose sensitive data
- Circular dependencies - Avoid complex Pick/Omit chains
Readonly and Record Utility Types
Readonly<T>
Makes all properties in T readonly.
Basic Usage
interface User {
id: string;
name: string;
email: string;
}
type ReadonlyUser = Readonly<User>;
// {
// readonly id: string;
// readonly name: string;
// readonly email: string;
// }
const user: ReadonlyUser = {
id: '123',
name: 'John',
email: 'john@example.com'
};
// user.name = 'Jane'; // ❌ Error: Cannot assign to 'name' because it is read-only
Implementation
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Use Cases
Immutable Configuration
interface Config {
apiUrl: string;
timeout: number;
retries: number;
}
const config: Readonly<Config> = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
// Prevent accidental modification
// config.timeout = 10000; // ❌ Error
Function Parameters
function processUsers(users: readonly User[]) {
// Cannot modify the array
// users.push({ ... }); // ❌ Error
// users[0].name = 'Jane'; // ❌ Error if User is Readonly<User>
return users.map(u => u.name);
}
Const Assertions (Better Alternative)
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
} as const;
// Type: {
// readonly apiUrl: "https://api.example.com";
// readonly timeout: 5000;
// readonly retries: 3;
// }
Deep Readonly
For nested objects:
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
interface Config {
server: {
host: string;
port: number;
};
database: {
host: string;
credentials: {
user: string;
password: string;
};
};
}
type ImmutableConfig = DeepReadonly<Config>;
const config: ImmutableConfig = { /* ... */ };
// config.server.host = 'localhost'; // ❌ Error
// config.database.credentials.user = 'admin'; // ❌ Error
Record<K, T>
Creates an object type with keys of type K and values of type T.
Basic Usage
type Roles = 'admin' | 'user' | 'guest';
type RolePermissions = Record<Roles, string[]>;
// {
// admin: string[];
// user: string[];
// guest: string[];
// }
const permissions: RolePermissions = {
admin: ['read', 'write', 'delete'],
user: ['read', 'write'],
guest: ['read']
};
Implementation
type Record<K extends keyof any, T> = {
[P in K]: T;
};
Use Cases
String Dictionary
type StringMap = Record<string, string>;
const translations: StringMap = {
hello: 'Hola',
goodbye: 'Adiós',
welcome: 'Bienvenido'
};
Enum-based Configurations
enum Status {
Pending = 'pending',
Approved = 'approved',
Rejected = 'rejected'
}
type StatusConfig = Record<Status, {
color: string;
label: string;
}>;
const statusConfig: StatusConfig = {
[Status.Pending]: { color: 'yellow', label: 'Pending Review' },
[Status.Approved]: { color: 'green', label: 'Approved' },
[Status.Rejected]: { color: 'red', label: 'Rejected' }
};
Type-safe State Handlers
type EventType = 'click' | 'hover' | 'focus';
type EventHandlers = Record<EventType, (event: Event) => void>;
const handlers: EventHandlers = {
click: (e) => console.log('clicked'),
hover: (e) => console.log('hovered'),
focus: (e) => console.log('focused')
};
API Endpoints
type Endpoint = 'users' | 'posts' | 'comments';
type ApiConfig = Record<Endpoint, {
url: string;
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
}>;
const api: ApiConfig = {
users: { url: '/api/users', method: 'GET' },
posts: { url: '/api/posts', method: 'GET' },
comments: { url: '/api/comments', method: 'GET' }
};
Combining Readonly and Record
Immutable Dictionary
type ImmutableMap<K extends keyof any, T> = Readonly<Record<K, T>>;
const colors: ImmutableMap<string, string> = {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745'
};
// colors.primary = '#0056b3'; // ❌ Error
// colors['danger'] = '#dc3545'; // ❌ Error (in readonly arrays)
Deep Readonly Record
type DeepReadonlyRecord<K extends keyof any, T> = {
readonly [P in K]: T extends object ? DeepReadonly<T> : T;
};
type Config = DeepReadonlyRecord<string, {
enabled: boolean;
options: {
timeout: number;
};
}>;
Partial Record
For optional keys:
type PartialRecord<K extends keyof any, T> = Partial<Record<K, T>>;
type OptionalPermissions = PartialRecord<Roles, string[]>;
const permissions: OptionalPermissions = {
admin: ['read', 'write']
// user and guest not required
};
Best Practices
- Use Readonly for immutability - Prevent accidental mutations
- Use Record for string-keyed objects - More explicit than index signatures
- Combine with const assertions - For literal types
- Consider readonly arrays -
readonly T[]orReadonlyArray<T> - Use DeepReadonly for nested data - Ensure complete immutability
Common Patterns
Lookup Tables
type HttpStatusCode = 200 | 400 | 404 | 500;
const statusMessages: Record<HttpStatusCode, string> = {
200: 'OK',
400: 'Bad Request',
404: 'Not Found',
500: 'Internal Server Error'
};
Feature Flags
type FeatureName = 'darkMode' | 'betaFeatures' | 'analytics';
type FeatureFlags = Record<FeatureName, boolean>;
const features: Readonly<FeatureFlags> = {
darkMode: true,
betaFeatures: false,
analytics: true
};
Error Codes
enum ErrorCode {
NetworkError = 'NETWORK_ERROR',
ValidationError = 'VALIDATION_ERROR',
AuthError = 'AUTH_ERROR'
}
type ErrorMessages = Record<ErrorCode, string>;
const errors: Readonly<ErrorMessages> = {
[ErrorCode.NetworkError]: 'Network connection failed',
[ErrorCode.ValidationError]: 'Invalid input data',
[ErrorCode.AuthError]: 'Authentication failed'
};
Common Pitfalls
- Shallow readonly - Nested objects are still mutable without DeepReadonly
- Runtime immutability - Readonly is compile-time only, use Object.freeze() for runtime
- Record with string keys - Might be too permissive, consider literal unions
- Assuming Record enforces all keys -
Record<string, T>allows any string key
ReturnType, Parameters & Function Utility Types
Function type extraction utilities for working with function signatures.
ReturnType<T>
Extracts the return type of a function type.
type GetUser = () => { id: string; name: string };
type User = ReturnType<GetUser>; // { id: string; name: string }
function fetchData(): Promise<{ data: string[] }> {
return Promise.resolve({ data: [] });
}
type FetchResult = ReturnType<typeof fetchData>; // Promise<{ data: string[] }>
Parameters<T>
Extracts function parameter types as a tuple.
function createUser(name: string, age: number, active: boolean) {
return { name, age, active };
}
type CreateUserParams = Parameters<typeof createUser>; // [string, number, boolean]
// Use in wrapper functions
function logAndCreate(...args: Parameters<typeof createUser>) {
console.log('Creating user:', args);
return createUser(...args);
}
ConstructorParameters<T>
Extracts constructor parameter types.
class User {
constructor(public name: string, public age: number) {}
}
type UserConstructorParams = ConstructorParameters<typeof User>; // [string, number]
// Factory pattern
function createInstance<T extends new (...args: any[]) => any>(
ctor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new ctor(...args);
}
InstanceType<T>
Extracts the instance type of a constructor function.
class Database {
connect() {}
query(sql: string) {}
}
type DB = InstanceType<typeof Database>; // Database
// With generic constructors
function createService<T extends new () => any>(
Service: T
): InstanceType<T> {
return new Service();
}
ThisParameterType<T>
Extracts the type of this parameter from a function.
interface User {
name: string;
greet(this: User): void;
}
type UserThis = ThisParameterType<User['greet']>; // User
OmitThisParameter<T>
Removes this parameter from function type.
function greet(this: User, message: string) {
console.log(`${this.name}: ${message}`);
}
type GreetFn = OmitThisParameter<typeof greet>; // (message: string) => void
Practical Patterns
API Client Type Safety
interface API {
getUser: (id: string) => Promise<User>;
createUser: (data: UserInput) => Promise<User>;
updateUser: (id: string, data: Partial<UserInput>) => Promise<User>;
}
// Extract all parameter types
type APIParams = {
[K in keyof API]: Parameters<API[K]>
};
// Result: { getUser: [string], createUser: [UserInput], ... }
// Extract all return types
type APIReturns = {
[K in keyof API]: ReturnType<API[K]>
};
Function Composition
function compose<A extends any[], B, C>(
f: (...args: A) => B,
g: (b: B) => C
): (...args: A) => C {
return (...args: A) => g(f(...args));
}
const add = (a: number, b: number) => a + b;
const toString = (n: number) => n.toString();
const addAndStringify = compose(add, toString);
// Type: (a: number, b: number) => string
Callback Type Extraction
function addEventListener(
event: string,
callback: (e: MouseEvent) => void
): void {
// Implementation
}
type EventCallback = Parameters<typeof addEventListener>[1];
// (e: MouseEvent) => void
Best Practices
- Use with typeof for values:
ReturnType<typeof fn>notReturnType<fn> - Prefer over manual extraction: Let TypeScript infer types from implementation
- Combine with generics: Create flexible utility functions
- Document extracted types: Add type aliases for clarity
- Use in factory patterns: Ensure type safety in creation functions
Common Patterns
Type-Safe Event Emitters
type EventMap = {
'user:created': (user: User) => void;
'user:updated': (id: string, user: User) => void;
'user:deleted': (id: string) => void;
};
class EventEmitter<T extends Record<string, (...args: any[]) => void>> {
on<K extends keyof T>(
event: K,
callback: T[K]
): void {}
emit<K extends keyof T>(
event: K,
...args: Parameters<T[K]>
): void {}
}
const emitter = new EventEmitter<EventMap>();
emitter.on('user:created', (user) => {}); // user: User
emitter.emit('user:updated', '123', user); // Type-safe
Middleware Typing
type Middleware<Req, Res> = (
req: Req,
res: Res,
next: () => void
) => void;
type ExtractMiddlewareReq<T> = T extends Middleware<infer R, any> ? R : never;
type ExtractMiddlewareRes<T> = T extends Middleware<any, infer R> ? R : never;
Anti-Patterns
❌ Manual type extraction
type BadReturn = { data: string[] }; // Hardcoded
✅ Use ReturnType
type GoodReturn = ReturnType<typeof fetchData>; // Derived
❌ Ignoring rest parameters
function wrapper(first: string) {} // Missing ...rest
✅ Spread Parameters
function wrapper(...args: Parameters<typeof original>) {
return original(...args);
}