9.8 KiB
name, description, allowed-tools, version
| name | description | allowed-tools | version |
|---|---|---|---|
| using-generics | Teaches generic constraints, avoiding any in generic defaults, and mapped types in TypeScript. Use when creating reusable functions, components, or types that work with multiple types while maintaining type safety. | Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite | 1.0.0 |
- 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
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.
## Generic Design FlowStep 1: Identify the Varying Type
What changes between uses?
- Data type in container (Array, Promise)
- 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.
In this skill:
references/detailed-examples.md- DeepPartial, FilterByType, conditional types, constructablesreferences/common-patterns.md- Array ops, object utils, Promise utils, buildersreferences/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
- 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)
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.
-
Constraints:
- Generic parameters constrained when accessing properties
keyofused for property key typesextendsused 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
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.