- •Creating reusable functions or classes
- •Designing generic APIs or libraries
- •Working with generic defaults (
<T = ...>) - •Implementing mapped types or conditional types
- •User mentions generics, type parameters, constraints, or reusable types </when-to-activate>
Key Concepts:
- •Generic Parameters:
<T>- Type variables that get filled in at call site - •Constraints:
<T extends Shape>- Limits what types T can be - •Defaults:
<T = string>- Fallback when type not provided - •Mapped Types: Transform existing types systematically
Impact: Write flexible, reusable code without sacrificing type safety. </overview>
<workflow> ## Generic Design FlowStep 1: Identify the Varying Type
What changes between uses?
- •Data type in container (Array<T>, Promise<T>)
- •Object shape variations
- •Return type based on input
- •Multiple related types
Step 2: Choose Constraint Strategy
No Constraint - Accepts any type
function identity<T>(value: T): T {
return value;
}
Extends Constraint - Requires specific shape
function logId<T extends { id: string }>(item: T): void {
console.log(item.id);
}
Union Constraint - Limited set of types
function process<T extends string | number>(value: T): T {
return value;
}
Multiple Constraints - Multiple type parameters with relationships
function merge<T extends object, U extends object>(a: T, b: U): T & U {
return { ...a, ...b };
}
Step 3: Set Default (If Needed)
Prefer no default over any default:
interface ApiResponse<T = unknown> { data: T; }
Or require explicit type parameter:
interface ApiResponse<T> { data: T; }
❌ No constraint (too permissive)
function getProperty<T>(obj: T, key: string): any {
return obj[key];
}
Problems:
- •
obj[key]not type-safe (T might not have string keys) - •Returns
any(loses type information) - •No IDE autocomplete for key
✅ Proper constraints
function getProperty<T extends object, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name");
const invalid = getProperty(user, "invalid");
Benefits:
- •Type-safe key access
- •Return type is
T[K](actual property type) - •IDE autocompletes valid keys
- •Compile error for invalid keys
Example 2: Generic Defaults
❌ Using any default (unsafe)
interface Result<T = any> {
data: T;
error?: string;
}
const result: Result = { data: "anything" };
result.data.nonExistentProperty;
✅ Using unknown default (safe)
interface Result<T = unknown> {
data: T;
error?: string;
}
const result: Result = { data: "anything" };
if (typeof result.data === "string") {
console.log(result.data.toUpperCase());
}
✅ No default (best)
interface Result<T> {
data: T;
error?: string;
}
const result: Result<string> = { data: "specific type" };
console.log(result.data.toUpperCase());
Example 3: Constraining Generic Parameters
Example: Ensuring object has id
interface HasId {
id: string;
}
function findById<T extends HasId>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
const users = [
{ id: "1", name: "Alice" },
{ id: "2", name: "Bob" }
];
const user = findById(users, "1");
Example: Ensuring constructable type
interface Constructable<T> {
new (...args: any[]): T;
}
function create<T>(Constructor: Constructable<T>): T {
return new Constructor();
}
class User {
name = "Anonymous";
}
const user = create(User);
Example: Ensuring array element type
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
const first = firstElement([1, 2, 3]);
const second = firstElement(["a", "b"]);
Example 4: Multiple Type Parameters
Example: Key-value mapping
function mapObject<T extends object, U>(
obj: T,
fn: (value: T[keyof T]) => U
): Record<keyof T, U> {
const result = {} as Record<keyof T, U>;
for (const key in obj) {
result[key] = fn(obj[key]);
}
return result;
}
const user = { name: "Alice", age: 30 };
const lengths = mapObject(user, val => String(val).length);
Example: Conditional return types
function parse<T extends "json" | "text">(
response: Response,
type: T
): T extends "json" ? Promise<unknown> : Promise<string> {
if (type === "json") {
return response.json() as any;
}
return response.text() as any;
}
const json = await parse(response, "json");
const text = await parse(response, "text");
Example 5: Mapped Types
Making properties optional:
type Partial<T> = {
[P in keyof T]?: T[P];
};
const partialUser: Partial<User> = { name: "Alice" };
Making properties readonly:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
Picking specific properties:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type UserPreview = Pick<User, "id" | "name">;
See references/detailed-examples.md for DeepPartial, FilterByType, and other complex mapped type patterns.
Example 6: Conditional Types
Unwrap promise type:
type Awaited<T> = T extends Promise<infer U> ? U : T;
Extract function parameters:
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
See references/detailed-examples.md for more conditional type patterns including FilterByType, nested promise unwrapping, and parameter extraction.
</examples>
In this skill:
- •
references/detailed-examples.md- DeepPartial, FilterByType, conditional types, constructables - •
references/common-patterns.md- Array ops, object utils, Promise utils, builders - •
references/advanced-patterns.md- Recursive generics, variadic tuples, branded types, HKTs
Related skills:
- •Use the using-type-guards skill for narrowing generic types
- •Use the avoiding-any-types skill for generic defaults
- •Use the using-runtime-checks skill for validating generic data </progressive-disclosure>
- •Use
extendsto constrain generic parameters when accessing properties - •Use
keyof Tfor type-safe property access - •Use
unknownfor generic defaults if truly dynamic - •Specify return type based on generic parameters
SHOULD:
- •Prefer no default over
anydefault - •Use descriptive type parameter names for complex generics
- •Infer type parameters from usage when possible
- •Use helper types (Pick, Omit, Partial) over manual mapping
NEVER:
- •Use
anyas generic default - •Access properties on unconstrained generics
- •Use
as anyto bypass generic constraints - •Create overly complex nested generics (split into smaller types) </constraints>
Array Operations
function last<T>(arr: T[]): T | undefined {
return arr[arr.length - 1];
}
function chunk<T>(arr: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
}
Object Utilities
function pick<T extends object, K extends keyof T>(
obj: T,
...keys: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = obj[key];
}
return result;
}
Class Generics
class Container<T> {
constructor(private value: T) {}
map<U>(fn: (value: T) => U): Container<U> {
return new Container(fn(this.value));
}
}
See references/common-patterns.md for complete implementations including Promise utilities, builders, event emitters, and more.
</patterns>
- •
Constraints:
- • Generic parameters constrained when accessing properties
- •
keyofused for property key types - •
extendsused appropriately
- •
Defaults:
- • No
anydefaults - •
unknownused for truly dynamic defaults - • Or no default (require explicit type)
- • No
- •
Type Inference:
- • Type parameters inferred from usage
- • Explicit types only when inference fails
- • Return types correctly derived from generics
- •
Complexity:
- • Generic types are understandable
- • Complex types split into smaller pieces
- • Helper types used appropriately </validation>
For advanced patterns including:
- •Recursive Generics (DeepReadonly, DeepPartial)
- •Variadic Tuple Types (type-safe array concatenation)
- •Template Literal Types (string manipulation at type level)
- •Branded Types (nominal typing in structural system)
- •Distributive Conditional Types
- •Higher-Kinded Types (simulation)
See references/advanced-patterns.md for detailed implementations and examples.
</advanced-patterns>