Claude Code Plugins

Community-maintained marketplace

Feedback

TypeScript utility types and type-safe API design patterns. Use when designing type systems, creating utility types, or implementing type-safe patterns.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name typescript-patterns
description TypeScript utility types and type-safe API design patterns. Use when designing type systems, creating utility types, or implementing type-safe patterns.

TypeScript Type Design Patterns

Utility Type Patterns

Built-in Utilities (Quick Reference)

// Selection
Pick<User, 'id' | 'name'>      // Select specific fields
Omit<User, 'password'>          // Exclude specific fields

// Modification
Partial<User>                   // All optional
Required<User>                  // All required
Readonly<User>                  // All readonly

// Mapping
Record<string, User>            // Object with User values

// Union manipulation
Exclude<'a' | 'b' | 'c', 'a'>  // 'b' | 'c'
Extract<'a' | 'b', 'a' | 'c'>  // 'a'
NonNullable<T | null>           // T

// Function utilities
ReturnType<typeof fn>           // Extract return type
Parameters<typeof fn>           // Extract parameter types

Custom Utility Types

Make specific fields optional:

type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type UserUpdate = Optional<User, 'email' | 'avatar'>;

Deep Partial:

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

Nullable:

type Nullable<T> = T | null;

Discriminated Unions

Pattern

type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

function handle<T>(result: Result<T>) {
  if (result.success) {
    // TypeScript knows result.data exists
    console.log(result.data);
  } else {
    // TypeScript knows result.error exists
    console.log(result.error);
  }
}

Critical: Discriminant must be a literal type (true, 'loading', etc.).

API Response Pattern

type ApiResponse<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

Generic Type Design

Constraints

// Constrain to objects with specific properties
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Constrain to specific shape
function processEntity<T extends { id: string }>(entity: T) {
  return entity.id;
}

// Multiple constraints
function merge<T extends object, U extends object>(a: T, b: U): T & U {
  return { ...a, ...b };
}

Default Type Parameters

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
}

// Can omit type parameter
const response: ApiResponse = { data: "anything", status: 200 };

// Or provide specific type
const typed: ApiResponse<User> = { data: user, status: 200 };

Type Guards

Custom Type Guards

interface User {
  type: 'user';
  name: string;
}

interface Admin {
  type: 'admin';
  name: string;
  permissions: string[];
}

// Type predicate
function isAdmin(user: User | Admin): user is Admin {
  return user.type === 'admin';
}

// Usage
if (isAdmin(user)) {
  // TypeScript knows user is Admin
  console.log(user.permissions);
}

Assertion Functions

function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
  if (value === null || value === undefined) {
    throw new Error('Value is null or undefined');
  }
}

function process(value: string | null) {
  assertIsDefined(value);
  // TypeScript knows value is string here
  return value.toUpperCase();
}

Conditional Types

Basic Pattern

type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

Extract Return Type

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getUser() {
  return { id: '1', name: 'Alice' };
}

type User = ReturnType<typeof getUser>;  // { id: string; name: string }

Unwrap Promise

type Unwrap<T> = T extends Promise<infer U> ? U : T;

type Result = Unwrap<Promise<string>>;  // string
type Direct = Unwrap<number>;           // number

Mapped Types

Transform Properties

// Make all properties optional
type Optional<T> = {
  [P in keyof T]?: T[P];
};

// Make all properties readonly
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

// Transform all to string
type Stringify<T> = {
  [P in keyof T]: string;
};

Conditional Property Types

// Make all properties nullable
type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

Template Literal Types

String Manipulation

// Capitalize
type Greeting = `Hello ${string}`;

// Event names
type EventName = 'click' | 'hover' | 'focus';
type Handler = `on${Capitalize<EventName>}`;
// 'onClick' | 'onHover' | 'onFocus'

// Combine literals
type Color = 'red' | 'blue';
type Size = 'sm' | 'lg';
type Variant = `${Color}-${Size}`;
// 'red-sm' | 'red-lg' | 'blue-sm' | 'blue-lg'

Type-Safe Event Emitters

type EventMap = {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string };
  'data:update': { id: string; data: unknown };
};

class TypedEmitter {
  on<K extends keyof EventMap>(
    event: K,
    handler: (data: EventMap[K]) => void
  ): void {
    // Implementation
  }

  emit<K extends keyof EventMap>(
    event: K,
    data: EventMap[K]
  ): void {
    // Implementation
  }
}

// Usage is fully type-safe
const emitter = new TypedEmitter();
emitter.on('user:login', (data) => {
  // data is typed as { userId: string; timestamp: Date }
  console.log(data.userId);
});

Branded Types

Create distinct types from same primitive:

type UserId = string & { readonly __brand: 'UserId' };
type ProductId = string & { readonly __brand: 'ProductId' };

function createUserId(id: string): UserId {
  return id as UserId;
}

function getUser(id: UserId) { /* ... */ }

const userId = createUserId('user-123');
const productId = 'product-456' as ProductId;

// This is a type error:
// getUser(productId);  // ProductId is not assignable to UserId

Use when: Need to distinguish same-typed values (IDs, tokens, etc.).

Interface vs Type

Use Interface for:

  • Object shapes
  • When extension is expected
  • Class contracts

Use Type for:

  • Unions
  • Tuples
  • Complex computed types
  • When using utility types
// Interface - extensible
interface User {
  id: string;
  name: string;
}

interface Admin extends User {
  role: 'admin';
}

// Type - unions
type Status = 'idle' | 'loading' | 'success' | 'error';

type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

tsconfig.json Essential Settings

{
  "compilerOptions": {
    // Strict type checking
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,
    "exactOptionalPropertyTypes": true,

    // Module resolution
    "moduleResolution": "bundler",
    "module": "ESNext",
    "target": "ES2022",

    // Emit
    "declaration": true,
    "sourceMap": true,

    // Interop
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,

    // Performance
    "skipLibCheck": true,

    // Path mapping
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

Always enable: strict, noUncheckedIndexedAccess, forceConsistentCasingInFileNames

Best Practices

1. Avoid any

// Bad
function process(data: any) {
  return data.value;
}

// Good - use proper types
function process(data: { value: string }) {
  return data.value;
}

// Good - use unknown for truly unknown
function parse(json: string): unknown {
  return JSON.parse(json);
}

2. Use const Assertions

// Instead of
const colors = ['red', 'blue']; // string[]

// Use const assertion
const colors = ['red', 'blue'] as const; // readonly ['red', 'blue']

// For objects
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
} as const;

3. Prefer Union Types Over Enums

// Instead of enum
enum Status {
  Idle = 'idle',
  Loading = 'loading',
}

// Prefer union type
type Status = 'idle' | 'loading' | 'success' | 'error';

Why: Simpler, more flexible, better tree-shaking.

4. Use readonly for Immutability

interface User {
  readonly id: string;
  name: string;
}

function processItems(items: readonly string[]) {
  // Cannot mutate items
  return items.join(',');
}

5. Type Narrowing

function process(value: string | number | null) {
  // Null check
  if (value === null) return 'null';

  // typeof check
  if (typeof value === 'string') {
    return value.toUpperCase();
  }

  // TypeScript knows value is number here
  return value.toFixed(2);
}

Common Pitfalls

Type assertions (as) - Use sparingly:

// Avoid when possible
const user = data as User;

// Better: Validate at runtime
if (isUser(data)) {
  // data is User here
}

Non-null assertion (!) - Dangerous:

// Avoid
const user = getUser()!;

// Better: Handle null
const user = getUser();
if (user) {
  // Use user safely
}

Empty object type {} - Too permissive:

// Bad
const config: {} = 'anything';

// Good
const config: object = { key: 'value' };
const settings: Record<string, unknown> = { key: 'value' };

Type Design Checklist

When designing types:

  • Use strict mode
  • Avoid any - use unknown or proper types
  • Use discriminated unions for state
  • Use const assertions for literal types
  • Prefer interfaces for objects, types for unions
  • Use readonly where appropriate
  • Provide good error messages (use descriptive types)
  • Consider future extension needs