Initial commit
This commit is contained in:
440
skills/using-generics/SKILL.md
Normal file
440
skills/using-generics/SKILL.md
Normal file
@@ -0,0 +1,440 @@
|
||||
---
|
||||
name: using-generics
|
||||
description: 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.
|
||||
allowed-tools: Read, Write, Edit, Glob, Grep, Bash, Task, TodoWrite
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
<role>
|
||||
This skill teaches how to use TypeScript generics effectively with proper constraints, avoiding `any` defaults, and leveraging mapped types for type transformations.
|
||||
</role>
|
||||
|
||||
<when-to-activate>
|
||||
This skill activates when:
|
||||
|
||||
- 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>
|
||||
|
||||
<overview>
|
||||
Generics enable writing reusable code that works with multiple types while preserving type safety. Proper use of constraints prevents `any` abuse and provides better IDE support.
|
||||
|
||||
**Key Concepts**:
|
||||
|
||||
1. **Generic Parameters**: `<T>` - Type variables that get filled in at call site
|
||||
2. **Constraints**: `<T extends Shape>` - Limits what types T can be
|
||||
3. **Defaults**: `<T = string>` - Fallback when type not provided
|
||||
4. **Mapped Types**: Transform existing types systematically
|
||||
|
||||
**Impact**: Write flexible, reusable code without sacrificing type safety.
|
||||
</overview>
|
||||
|
||||
<workflow>
|
||||
## Generic Design Flow
|
||||
|
||||
**Step 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
|
||||
```typescript
|
||||
function identity<T>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
**Extends Constraint** - Requires specific shape
|
||||
```typescript
|
||||
function logId<T extends { id: string }>(item: T): void {
|
||||
console.log(item.id);
|
||||
}
|
||||
```
|
||||
|
||||
**Union Constraint** - Limited set of types
|
||||
```typescript
|
||||
function process<T extends string | number>(value: T): T {
|
||||
return value;
|
||||
}
|
||||
```
|
||||
|
||||
**Multiple Constraints** - Multiple type parameters with relationships
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
interface ApiResponse<T = unknown> { data: T; }
|
||||
```
|
||||
|
||||
Or require explicit type parameter:
|
||||
```typescript
|
||||
interface ApiResponse<T> { data: T; }
|
||||
```
|
||||
</workflow>
|
||||
|
||||
<examples>
|
||||
## Example 1: Generic Function Constraints
|
||||
|
||||
**❌ No constraint (too permissive)**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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)**
|
||||
|
||||
```typescript
|
||||
interface Result<T = any> {
|
||||
data: T;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
const result: Result = { data: "anything" };
|
||||
result.data.nonExistentProperty;
|
||||
```
|
||||
|
||||
**✅ Using `unknown` default (safe)**
|
||||
|
||||
```typescript
|
||||
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)**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
type Partial<T> = {
|
||||
[P in keyof T]?: T[P];
|
||||
};
|
||||
|
||||
const partialUser: Partial<User> = { name: "Alice" };
|
||||
```
|
||||
|
||||
**Making properties readonly:**
|
||||
|
||||
```typescript
|
||||
type Readonly<T> = {
|
||||
readonly [P in keyof T]: T[P];
|
||||
};
|
||||
```
|
||||
|
||||
**Picking specific properties:**
|
||||
|
||||
```typescript
|
||||
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:**
|
||||
|
||||
```typescript
|
||||
type Awaited<T> = T extends Promise<infer U> ? U : T;
|
||||
```
|
||||
|
||||
**Extract function parameters:**
|
||||
|
||||
```typescript
|
||||
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>
|
||||
|
||||
<progressive-disclosure>
|
||||
## Reference Files
|
||||
|
||||
**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>
|
||||
|
||||
<constraints>
|
||||
**MUST:**
|
||||
|
||||
- Use `extends` to constrain generic parameters when accessing properties
|
||||
- Use `keyof T` for type-safe property access
|
||||
- Use `unknown` for generic defaults if truly dynamic
|
||||
- Specify return type based on generic parameters
|
||||
|
||||
**SHOULD:**
|
||||
|
||||
- Prefer no default over `any` default
|
||||
- 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 `any` as generic default
|
||||
- Access properties on unconstrained generics
|
||||
- Use `as any` to bypass generic constraints
|
||||
- Create overly complex nested generics (split into smaller types)
|
||||
</constraints>
|
||||
|
||||
<patterns>
|
||||
## Common Generic Patterns
|
||||
|
||||
### Array Operations
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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>
|
||||
|
||||
<validation>
|
||||
## Generic Type Safety Checklist
|
||||
|
||||
1. **Constraints**:
|
||||
- [ ] Generic parameters constrained when accessing properties
|
||||
- [ ] `keyof` used for property key types
|
||||
- [ ] `extends` used appropriately
|
||||
|
||||
2. **Defaults**:
|
||||
- [ ] No `any` defaults
|
||||
- [ ] `unknown` used for truly dynamic defaults
|
||||
- [ ] Or no default (require explicit type)
|
||||
|
||||
3. **Type Inference**:
|
||||
- [ ] Type parameters inferred from usage
|
||||
- [ ] Explicit types only when inference fails
|
||||
- [ ] Return types correctly derived from generics
|
||||
|
||||
4. **Complexity**:
|
||||
- [ ] Generic types are understandable
|
||||
- [ ] Complex types split into smaller pieces
|
||||
- [ ] Helper types used appropriately
|
||||
</validation>
|
||||
|
||||
<advanced-patterns>
|
||||
## Advanced Generic Patterns
|
||||
|
||||
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>
|
||||
168
skills/using-generics/references/advanced-patterns.md
Normal file
168
skills/using-generics/references/advanced-patterns.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Advanced Generic Patterns
|
||||
|
||||
## Recursive Generics
|
||||
|
||||
Use recursive generics to apply transformations deeply through nested object structures:
|
||||
|
||||
```typescript
|
||||
type DeepReadonly<T> = {
|
||||
readonly [P in keyof T]: T[P] extends object
|
||||
? DeepReadonly<T[P]>
|
||||
: T[P];
|
||||
};
|
||||
|
||||
interface Config {
|
||||
database: {
|
||||
host: string;
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const config: DeepReadonly<Config> = {
|
||||
database: {
|
||||
host: "localhost",
|
||||
credentials: {
|
||||
username: "admin",
|
||||
password: "secret"
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
## Variadic Tuple Types
|
||||
|
||||
TypeScript 4.0+ supports variadic tuple types for precise array concatenation:
|
||||
|
||||
```typescript
|
||||
function concat<T extends readonly unknown[], U extends readonly unknown[]>(
|
||||
arr1: T,
|
||||
arr2: U
|
||||
): [...T, ...U] {
|
||||
return [...arr1, ...arr2];
|
||||
}
|
||||
|
||||
const result = concat([1, 2], ["a", "b"]);
|
||||
|
||||
function curry<T extends unknown[], U extends unknown[], R>(
|
||||
fn: (...args: [...T, ...U]) => R,
|
||||
...first: T
|
||||
): (...args: U) => R {
|
||||
return (...rest: U) => fn(...first, ...rest);
|
||||
}
|
||||
```
|
||||
|
||||
## Template Literal Types
|
||||
|
||||
Combine string literals with generics for type-safe string manipulation:
|
||||
|
||||
```typescript
|
||||
type EventName<T extends string> = `on${Capitalize<T>}`;
|
||||
|
||||
type ClickEvent = EventName<"click">;
|
||||
type HoverEvent = EventName<"hover">;
|
||||
|
||||
type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
|
||||
type Endpoint<M extends HTTPMethod, P extends string> = `${M} ${P}`;
|
||||
|
||||
type UserEndpoint = Endpoint<"GET", "/users/:id">;
|
||||
```
|
||||
|
||||
## Branded Types
|
||||
|
||||
Create nominal types in TypeScript's structural type system:
|
||||
|
||||
```typescript
|
||||
type Brand<K, T> = K & { __brand: T };
|
||||
|
||||
type UserId = Brand<string, "UserId">;
|
||||
type EmailAddress = Brand<string, "Email">;
|
||||
type OrderId = Brand<string, "OrderId">;
|
||||
|
||||
function sendEmail(to: EmailAddress, from: EmailAddress): void {
|
||||
console.log(`Sending email from ${from} to ${to}`);
|
||||
}
|
||||
|
||||
function getUser(id: UserId): void {
|
||||
console.log(`Fetching user ${id}`);
|
||||
}
|
||||
|
||||
const email = "user@example.com" as EmailAddress;
|
||||
const userId = "user-123" as UserId;
|
||||
|
||||
sendEmail(email, email);
|
||||
getUser(userId);
|
||||
```
|
||||
|
||||
## Distributive Conditional Types
|
||||
|
||||
Conditional types distribute over unions when the checked type is a naked type parameter:
|
||||
|
||||
```typescript
|
||||
type ToArray<T> = T extends any ? T[] : never;
|
||||
|
||||
type Nums = ToArray<number | string>;
|
||||
|
||||
type NonNullable<T> = T extends null | undefined ? never : T;
|
||||
|
||||
type SafeString = NonNullable<string | null | undefined>;
|
||||
```
|
||||
|
||||
## Mapped Type Modifiers
|
||||
|
||||
Use `+`, `-`, `readonly`, and `?` modifiers in mapped types:
|
||||
|
||||
```typescript
|
||||
type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
type Required<T> = {
|
||||
[P in keyof T]-?: T[P];
|
||||
};
|
||||
|
||||
type ReadonlyPartial<T> = {
|
||||
+readonly [P in keyof T]+?: T[P];
|
||||
};
|
||||
```
|
||||
|
||||
## Inference in Conditional Types
|
||||
|
||||
Use `infer` to extract types within conditional types:
|
||||
|
||||
```typescript
|
||||
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
|
||||
|
||||
type ArrayElement<T> = T extends (infer E)[] ? E : never;
|
||||
|
||||
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
|
||||
|
||||
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any
|
||||
? F
|
||||
: never;
|
||||
```
|
||||
|
||||
## Higher-Kinded Types (Simulation)
|
||||
|
||||
While TypeScript doesn't have true HKTs, you can simulate them:
|
||||
|
||||
```typescript
|
||||
interface Functor<F> {
|
||||
map<A, B>(fa: HKT<F, A>, f: (a: A) => B): HKT<F, B>;
|
||||
}
|
||||
|
||||
interface HKT<F, A> {
|
||||
_F: F;
|
||||
_A: A;
|
||||
}
|
||||
|
||||
interface ArrayF {}
|
||||
|
||||
type ArrayHKT<A> = HKT<ArrayF, A> & A[];
|
||||
|
||||
const arrayFunctor: Functor<ArrayF> = {
|
||||
map: (fa, f) => fa.map(f)
|
||||
};
|
||||
```
|
||||
332
skills/using-generics/references/common-patterns.md
Normal file
332
skills/using-generics/references/common-patterns.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# Common Generic Patterns
|
||||
|
||||
## Array Operations
|
||||
|
||||
Generic array utilities maintain type safety while providing reusable functionality:
|
||||
|
||||
```typescript
|
||||
function last<T>(arr: T[]): T | undefined {
|
||||
return arr[arr.length - 1];
|
||||
}
|
||||
|
||||
function first<T>(arr: T[]): T | undefined {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
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 flatten<T>(arr: T[][]): T[] {
|
||||
return arr.reduce((acc, item) => acc.concat(item), []);
|
||||
}
|
||||
|
||||
function unique<T>(arr: T[]): T[] {
|
||||
return Array.from(new Set(arr));
|
||||
}
|
||||
|
||||
function partition<T>(arr: T[], predicate: (item: T) => boolean): [T[], T[]] {
|
||||
const truthy: T[] = [];
|
||||
const falsy: T[] = [];
|
||||
|
||||
for (const item of arr) {
|
||||
if (predicate(item)) {
|
||||
truthy.push(item);
|
||||
} else {
|
||||
falsy.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [truthy, falsy];
|
||||
}
|
||||
```
|
||||
|
||||
## Object Utilities
|
||||
|
||||
Type-safe object manipulation:
|
||||
|
||||
```typescript
|
||||
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;
|
||||
}
|
||||
|
||||
function omit<T extends object, K extends keyof T>(
|
||||
obj: T,
|
||||
...keys: K[]
|
||||
): Omit<T, K> {
|
||||
const result = { ...obj };
|
||||
for (const key of keys) {
|
||||
delete result[key];
|
||||
}
|
||||
return result as Omit<T, K>;
|
||||
}
|
||||
|
||||
function keys<T extends object>(obj: T): (keyof T)[] {
|
||||
return Object.keys(obj) as (keyof T)[];
|
||||
}
|
||||
|
||||
function values<T extends object>(obj: T): T[keyof T][] {
|
||||
return Object.values(obj) as T[keyof T][];
|
||||
}
|
||||
|
||||
function entries<T extends object>(obj: T): [keyof T, T[keyof T]][] {
|
||||
return Object.entries(obj) as [keyof T, T[keyof T]][];
|
||||
}
|
||||
|
||||
function mapValues<T extends object, U>(
|
||||
obj: T,
|
||||
fn: (value: T[keyof T], key: keyof T) => U
|
||||
): Record<keyof T, U> {
|
||||
const result = {} as Record<keyof T, U>;
|
||||
|
||||
for (const key in obj) {
|
||||
result[key] = fn(obj[key], key);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## Promise Utilities
|
||||
|
||||
Generic async operations:
|
||||
|
||||
```typescript
|
||||
async function retry<T>(
|
||||
fn: () => Promise<T>,
|
||||
maxAttempts: number
|
||||
): Promise<T> {
|
||||
let lastError: Error;
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
lastError = error as Error;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError!;
|
||||
}
|
||||
|
||||
async function timeout<T>(
|
||||
promise: Promise<T>,
|
||||
ms: number
|
||||
): Promise<T> {
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
setTimeout(() => reject(new Error("Timeout")), ms);
|
||||
});
|
||||
|
||||
return Promise.race([promise, timeoutPromise]);
|
||||
}
|
||||
|
||||
async function sequence<T>(
|
||||
promises: (() => Promise<T>)[]
|
||||
): Promise<T[]> {
|
||||
const results: T[] = [];
|
||||
|
||||
for (const promiseFn of promises) {
|
||||
results.push(await promiseFn());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
## Class Generics
|
||||
|
||||
Generic classes for reusable data structures:
|
||||
|
||||
```typescript
|
||||
class Container<T> {
|
||||
constructor(private value: T) {}
|
||||
|
||||
getValue(): T {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
setValue(value: T): void {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Container<U> {
|
||||
return new Container(fn(this.value));
|
||||
}
|
||||
|
||||
flatMap<U>(fn: (value: T) => Container<U>): Container<U> {
|
||||
return fn(this.value);
|
||||
}
|
||||
}
|
||||
|
||||
class Result<T, E = Error> {
|
||||
private constructor(
|
||||
private readonly value?: T,
|
||||
private readonly error?: E
|
||||
) {}
|
||||
|
||||
static ok<T, E = Error>(value: T): Result<T, E> {
|
||||
return new Result(value, undefined);
|
||||
}
|
||||
|
||||
static err<T, E = Error>(error: E): Result<T, E> {
|
||||
return new Result(undefined, error);
|
||||
}
|
||||
|
||||
isOk(): this is Result<T, never> {
|
||||
return this.value !== undefined;
|
||||
}
|
||||
|
||||
isErr(): this is Result<never, E> {
|
||||
return this.error !== undefined;
|
||||
}
|
||||
|
||||
unwrap(): T {
|
||||
if (this.error !== undefined) {
|
||||
throw this.error;
|
||||
}
|
||||
return this.value!;
|
||||
}
|
||||
|
||||
map<U>(fn: (value: T) => U): Result<U, E> {
|
||||
if (this.error !== undefined) {
|
||||
return Result.err(this.error);
|
||||
}
|
||||
return Result.ok(fn(this.value!));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Builder Pattern
|
||||
|
||||
Fluent API with type safety:
|
||||
|
||||
```typescript
|
||||
class QueryBuilder<T extends object> {
|
||||
private filters: Array<(item: T) => boolean> = [];
|
||||
private sortFn?: (a: T, b: T) => number;
|
||||
private limitValue?: number;
|
||||
|
||||
where<K extends keyof T>(key: K, value: T[K]): this {
|
||||
this.filters.push(item => item[key] === value);
|
||||
return this;
|
||||
}
|
||||
|
||||
whereFn(predicate: (item: T) => boolean): this {
|
||||
this.filters.push(predicate);
|
||||
return this;
|
||||
}
|
||||
|
||||
sort<K extends keyof T>(key: K, direction: "asc" | "desc" = "asc"): this {
|
||||
this.sortFn = (a, b) => {
|
||||
const aVal = a[key];
|
||||
const bVal = b[key];
|
||||
|
||||
if (aVal < bVal) return direction === "asc" ? -1 : 1;
|
||||
if (aVal > bVal) return direction === "asc" ? 1 : -1;
|
||||
return 0;
|
||||
};
|
||||
return this;
|
||||
}
|
||||
|
||||
limit(n: number): this {
|
||||
this.limitValue = n;
|
||||
return this;
|
||||
}
|
||||
|
||||
execute(data: T[]): T[] {
|
||||
let result = data.filter(item =>
|
||||
this.filters.every(filter => filter(item))
|
||||
);
|
||||
|
||||
if (this.sortFn) {
|
||||
result = result.sort(this.sortFn);
|
||||
}
|
||||
|
||||
if (this.limitValue !== undefined) {
|
||||
result = result.slice(0, this.limitValue);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const users = [
|
||||
{ id: 1, name: "Alice", active: true, age: 30 },
|
||||
{ id: 2, name: "Bob", active: false, age: 25 },
|
||||
{ id: 3, name: "Charlie", active: true, age: 35 }
|
||||
];
|
||||
|
||||
const active = new QueryBuilder<typeof users[0]>()
|
||||
.where("active", true)
|
||||
.sort("age", "desc")
|
||||
.limit(5)
|
||||
.execute(users);
|
||||
```
|
||||
|
||||
## Event Emitter Pattern
|
||||
|
||||
Type-safe event handling:
|
||||
|
||||
```typescript
|
||||
type EventMap = Record<string, any>;
|
||||
|
||||
class TypedEventEmitter<Events extends EventMap> {
|
||||
private listeners: {
|
||||
[K in keyof Events]?: Array<(data: Events[K]) => void>;
|
||||
} = {};
|
||||
|
||||
on<K extends keyof Events>(
|
||||
event: K,
|
||||
listener: (data: Events[K]) => void
|
||||
): this {
|
||||
if (!this.listeners[event]) {
|
||||
this.listeners[event] = [];
|
||||
}
|
||||
this.listeners[event]!.push(listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
emit<K extends keyof Events>(event: K, data: Events[K]): void {
|
||||
const listeners = this.listeners[event];
|
||||
if (listeners) {
|
||||
for (const listener of listeners) {
|
||||
listener(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
off<K extends keyof Events>(
|
||||
event: K,
|
||||
listener: (data: Events[K]) => void
|
||||
): this {
|
||||
const listeners = this.listeners[event];
|
||||
if (listeners) {
|
||||
this.listeners[event] = listeners.filter(l => l !== listener);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
interface AppEvents {
|
||||
userLogin: { userId: string; timestamp: number };
|
||||
userLogout: { userId: string };
|
||||
dataUpdated: { id: string; changes: Record<string, unknown> };
|
||||
}
|
||||
|
||||
const emitter = new TypedEventEmitter<AppEvents>();
|
||||
|
||||
emitter.on("userLogin", (data) => {
|
||||
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
|
||||
});
|
||||
```
|
||||
305
skills/using-generics/references/detailed-examples.md
Normal file
305
skills/using-generics/references/detailed-examples.md
Normal file
@@ -0,0 +1,305 @@
|
||||
# Detailed Generic Examples
|
||||
|
||||
## Deep Partial Implementation
|
||||
|
||||
The `DeepPartial` type recursively makes all properties optional, including nested objects:
|
||||
|
||||
```typescript
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
|
||||
interface DatabaseConfig {
|
||||
host: string;
|
||||
port: number;
|
||||
credentials: {
|
||||
username: string;
|
||||
password: string;
|
||||
ssl: {
|
||||
enabled: boolean;
|
||||
cert: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const partialConfig: DeepPartial<DatabaseConfig> = {
|
||||
host: "localhost",
|
||||
credentials: {
|
||||
username: "admin",
|
||||
ssl: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function mergeConfig(
|
||||
defaults: DatabaseConfig,
|
||||
override: DeepPartial<DatabaseConfig>
|
||||
): DatabaseConfig {
|
||||
return {
|
||||
...defaults,
|
||||
...override,
|
||||
credentials: {
|
||||
...defaults.credentials,
|
||||
...override.credentials,
|
||||
ssl: {
|
||||
...defaults.credentials.ssl,
|
||||
...override.credentials?.ssl
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Conditional Return Types
|
||||
|
||||
Use conditional types to change return types based on input:
|
||||
|
||||
```typescript
|
||||
type ParseResult<T extends "json" | "text"> =
|
||||
T extends "json" ? unknown : string;
|
||||
|
||||
function parse<T extends "json" | "text">(
|
||||
response: Response,
|
||||
type: T
|
||||
): Promise<ParseResult<T>> {
|
||||
if (type === "json") {
|
||||
return response.json() as Promise<ParseResult<T>>;
|
||||
}
|
||||
return response.text() as Promise<ParseResult<T>>;
|
||||
}
|
||||
|
||||
async function example() {
|
||||
const json = await parse(response, "json");
|
||||
|
||||
const text = await parse(response, "text");
|
||||
}
|
||||
|
||||
type QueryResult<T extends boolean> = T extends true
|
||||
? Array<{ id: string; data: unknown }>
|
||||
: { id: string; data: unknown } | undefined;
|
||||
|
||||
function query<Multiple extends boolean = false>(
|
||||
sql: string,
|
||||
multiple?: Multiple
|
||||
): QueryResult<Multiple> {
|
||||
if (multiple) {
|
||||
return [] as QueryResult<Multiple>;
|
||||
}
|
||||
return undefined as QueryResult<Multiple>;
|
||||
}
|
||||
|
||||
const single = query("SELECT * FROM users WHERE id = 1");
|
||||
const many = query("SELECT * FROM users", true);
|
||||
```
|
||||
|
||||
## Filter by Type Pattern
|
||||
|
||||
Extract only properties of a specific type from an object:
|
||||
|
||||
```typescript
|
||||
type FilterByType<T, U> = {
|
||||
[P in keyof T as T[P] extends U ? P : never]: T[P];
|
||||
};
|
||||
|
||||
interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
age: number;
|
||||
active: boolean;
|
||||
createdAt: Date;
|
||||
score: number;
|
||||
tags: string[];
|
||||
}
|
||||
|
||||
type StringProps = FilterByType<User, string>;
|
||||
|
||||
type NumberProps = FilterByType<User, number>;
|
||||
|
||||
type FilterByValueType<T, U> = Pick<
|
||||
T,
|
||||
{
|
||||
[K in keyof T]: T[K] extends U ? K : never;
|
||||
}[keyof T]
|
||||
>;
|
||||
|
||||
type BooleanProps = FilterByValueType<User, boolean>;
|
||||
```
|
||||
|
||||
## Unwrap Nested Promises
|
||||
|
||||
Extract the final resolved type from nested Promises:
|
||||
|
||||
```typescript
|
||||
type Awaited<T> = T extends Promise<infer U>
|
||||
? Awaited<U>
|
||||
: T;
|
||||
|
||||
type A = Awaited<Promise<string>>;
|
||||
|
||||
type B = Awaited<Promise<Promise<number>>>;
|
||||
|
||||
type C = Awaited<Promise<Promise<Promise<boolean>>>>;
|
||||
|
||||
async function deepFetch(): Promise<Promise<{ data: string }>> {
|
||||
return Promise.resolve(
|
||||
Promise.resolve({ data: "nested" })
|
||||
);
|
||||
}
|
||||
|
||||
type DeepFetchResult = Awaited<ReturnType<typeof deepFetch>>;
|
||||
```
|
||||
|
||||
## Extract Function Parameters
|
||||
|
||||
Get parameter types from function signatures:
|
||||
|
||||
```typescript
|
||||
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
|
||||
|
||||
function processUser(id: string, name: string, age: number): void {
|
||||
console.log(id, name, age);
|
||||
}
|
||||
|
||||
type ProcessUserParams = Parameters<typeof processUser>;
|
||||
|
||||
function callWithParams<F extends (...args: any[]) => any>(
|
||||
fn: F,
|
||||
...args: Parameters<F>
|
||||
): ReturnType<F> {
|
||||
return fn(...args);
|
||||
}
|
||||
|
||||
callWithParams(processUser, "123", "Alice", 30);
|
||||
|
||||
type FirstParameter<T> = T extends (first: infer F, ...args: any[]) => any
|
||||
? F
|
||||
: never;
|
||||
|
||||
type FirstParam = FirstParameter<typeof processUser>;
|
||||
```
|
||||
|
||||
## Constructable Types
|
||||
|
||||
Work with classes and constructors generically:
|
||||
|
||||
```typescript
|
||||
interface Constructable<T> {
|
||||
new (...args: any[]): T;
|
||||
}
|
||||
|
||||
function create<T>(Constructor: Constructable<T>): T {
|
||||
return new Constructor();
|
||||
}
|
||||
|
||||
function createWithArgs<T, Args extends any[]>(
|
||||
Constructor: new (...args: Args) => T,
|
||||
...args: Args
|
||||
): T {
|
||||
return new Constructor(...args);
|
||||
}
|
||||
|
||||
class User {
|
||||
constructor(public name: string, public age: number) {}
|
||||
}
|
||||
|
||||
const user1 = create(User);
|
||||
const user2 = createWithArgs(User, "Alice", 30);
|
||||
|
||||
function inject<T>(
|
||||
Constructor: Constructable<T>,
|
||||
dependencies: Map<Constructable<any>, any>
|
||||
): T {
|
||||
const args = getDependencies(Constructor, dependencies);
|
||||
return new Constructor(...args);
|
||||
}
|
||||
```
|
||||
|
||||
## Mapped Type Transformations
|
||||
|
||||
Complex property transformations using mapped types:
|
||||
|
||||
```typescript
|
||||
type Getters<T> = {
|
||||
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
|
||||
};
|
||||
|
||||
interface Person {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
type PersonGetters = Getters<Person>;
|
||||
|
||||
type Setters<T> = {
|
||||
[K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
|
||||
};
|
||||
|
||||
type PersonSetters = Setters<Person>;
|
||||
|
||||
type ReadonlyKeys<T> = {
|
||||
[K in keyof T]-?: T[K] extends { readonly [key: string]: any }
|
||||
? K
|
||||
: never;
|
||||
}[keyof T];
|
||||
|
||||
type WritableKeys<T> = {
|
||||
[K in keyof T]-?: T[K] extends { readonly [key: string]: any }
|
||||
? never
|
||||
: K;
|
||||
}[keyof T];
|
||||
```
|
||||
|
||||
## Type-Safe Event Handlers
|
||||
|
||||
Generic event handler system with strict typing:
|
||||
|
||||
```typescript
|
||||
interface EventHandlers<T> {
|
||||
onCreate?: (item: T) => void;
|
||||
onUpdate?: (item: T, changes: Partial<T>) => void;
|
||||
onDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
class Store<T extends { id: string }> {
|
||||
private items = new Map<string, T>();
|
||||
private handlers: EventHandlers<T> = {};
|
||||
|
||||
setHandlers(handlers: EventHandlers<T>): void {
|
||||
this.handlers = handlers;
|
||||
}
|
||||
|
||||
create(item: T): void {
|
||||
this.items.set(item.id, item);
|
||||
this.handlers.onCreate?.(item);
|
||||
}
|
||||
|
||||
update(id: string, changes: Partial<T>): void {
|
||||
const item = this.items.get(id);
|
||||
if (item) {
|
||||
const updated = { ...item, ...changes };
|
||||
this.items.set(id, updated);
|
||||
this.handlers.onUpdate?.(updated, changes);
|
||||
}
|
||||
}
|
||||
|
||||
delete(id: string): void {
|
||||
this.items.delete(id);
|
||||
this.handlers.onDelete?.(id);
|
||||
}
|
||||
}
|
||||
|
||||
interface Todo {
|
||||
id: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
}
|
||||
|
||||
const todoStore = new Store<Todo>();
|
||||
|
||||
todoStore.setHandlers({
|
||||
onCreate: (todo) => console.log("Created:", todo.title),
|
||||
onUpdate: (todo, changes) => console.log("Updated:", changes),
|
||||
onDelete: (id) => console.log("Deleted:", id)
|
||||
});
|
||||
```
|
||||
Reference in New Issue
Block a user