Teaches generic constraints, avoiding any in generic defaults, and mapped types in TypeScript...
<T = ...>)Key Concepts:
<T> - Type variables that get filled in at call site<T extends Shape> - Limits what types T can be<T = string> - Fallback when type not providedImpact: Write flexible, reusable code without sacrificing type safety.
Step 1: Identify the Varying Type
What changes between uses?
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)any (loses type information)✅ 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:
T[K] (actual property type)❌ 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: 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: 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");
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.
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, HKTsRelated skills:
extends to constrain generic parameters when accessing propertieskeyof T for type-safe property accessunknown for generic defaults if truly dynamicSHOULD:
any defaultNEVER:
any as generic defaultas any to bypass generic constraintsfunction 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;
}
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 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:
keyof used for property key typesextends used appropriatelyDefaults:
any defaultsunknown used for truly dynamic defaultsType Inference:
Complexity:
For advanced patterns including:
See references/advanced-patterns.md for detailed implementations and examples.