Files
gh-djankies-claude-configs-…/skills/using-generics/SKILL.md
2025-11-29 18:22:35 +08:00

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
This skill teaches how to use TypeScript generics effectively with proper constraints, avoiding `any` defaults, and leveraging mapped types for type transformations. 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
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.

## Generic Design Flow

Step 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; }
## Example 1: Generic Function Constraints

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.

## 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
**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)
## Common Generic Patterns

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.

## 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
## 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.