dry-types

📁 marius-townhouse/effective-typescript-skills 📅 10 days ago
1
总安装量
1
周安装量
#55290
全站排名
安装命令
npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill dry-types

Agent 安装分布

mcpjam 1
openhands 1
replit 1
windsurf 1
zencoder 1

Skill 文档

Use Type Operations and Generic Types to Avoid Repeating Yourself

Overview

Apply DRY (Don’t Repeat Yourself) to types, not just code.

Type duplication causes the same problems as code duplication: inconsistency, maintenance burden, and bugs. Use TypeScript’s type operations to derive types from other types.

When to Use This Skill

  • Copying fields between interfaces
  • Multiple types share common properties
  • Want one type to be a subset of another
  • Types should stay in sync automatically
  • Need to create optional/partial versions of types

The Iron Rule

NEVER copy-paste type definitions. Derive types from a single source of truth.

Remember:

  • extends for adding fields
  • Pick<T, K> for selecting fields
  • Partial<T> for making fields optional
  • keyof for getting key types
  • typeof for deriving types from values

Detection: The Copied Type Problem

If you see similar types diverging:

// ❌ Duplicated type definitions
interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate {
  firstName: string;   // Duplicated!
  lastName: string;    // Duplicated!
  birth: Date;
}

What if you add middleName to Person? Now they’re out of sync.

Basic Techniques

Use extends to Add Fields

// ✅ Derive from base type
interface Person {
  firstName: string;
  lastName: string;
}

interface PersonWithBirthDate extends Person {
  birth: Date;
}

Use Pick to Select Fields

interface State {
  userId: string;
  pageTitle: string;
  recentFiles: string[];
  pageContents: string;
}

// ✅ Select only the fields you need
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;

Use Partial for Optional Versions

interface Options {
  width: number;
  height: number;
  color: string;
}

class UIWidget {
  constructor(init: Options) { /* ... */ }
  
  // ✅ All fields optional for updates
  update(options: Partial<Options>) { /* ... */ }
}

Use keyof for Key Types

type OptionsKeys = keyof Options;
//   ^? type OptionsKeys = "width" | "height" | "color"

Use typeof to Derive from Values

const DEFAULTS = {
  width: 640,
  height: 480,
  color: '#00FF00',
};

// ✅ Type derived from value
type Options = typeof DEFAULTS;
//   ^? type Options = { width: number; height: number; color: string; }

Standard Library Utility Types

Utility Purpose Example
Pick<T, K> Select properties Pick<User, 'id' | 'name'>
Omit<T, K> Remove properties Omit<User, 'password'>
Partial<T> Make all optional Partial<Config>
Required<T> Make all required Required<PartialConfig>
Readonly<T> Make all readonly Readonly<State>
ReturnType<F> Function return type ReturnType<typeof fn>
Parameters<F> Function params Parameters<typeof fn>

Advanced Patterns

Mapped Types

// Create optional version manually
type OptionsUpdate = {
  [K in keyof Options]?: Options[K]
};

// Equivalent to Partial<Options>

Indexing into Union Types

interface SaveAction { type: 'save'; /* ... */ }
interface LoadAction { type: 'load'; /* ... */ }
type Action = SaveAction | LoadAction;

// ✅ Extract discriminant type
type ActionType = Action['type'];
//   ^? type ActionType = "save" | "load"

ReturnType for Function Results

function getUserInfo(userId: string) {
  return {
    userId,
    name,
    age,
    // ... many fields
  };
}

// ✅ Derive type from function
type UserInfo = ReturnType<typeof getUserInfo>;

Key Remapping with as

interface ShortToLong {
  q: 'search';
  n: 'numberOfResults';
}

// Invert the mapping
type LongToShort = {
  [K in keyof ShortToLong as ShortToLong[K]]: K
};
//   ^? type LongToShort = { search: "q"; numberOfResults: "n"; }

Homomorphic Mapped Types

Mapped types preserve modifiers when using keyof:

interface Customer {
  /** How the customer would like to be addressed. */
  title?: string;
  /** Complete name as entered in the system. */
  readonly name: string;
}

// ✅ Preserves optional and readonly
type PickTitle = Pick<Customer, 'title'>;
//   ^? type PickTitle = { title?: string; }

type PickName = Pick<Customer, 'name'>;
//   ^? type PickName = { readonly name: string; }

When NOT to Apply DRY

Don’t factor out types that are only coincidentally similar:

// ❌ Don't do this - coincidental similarity
interface NamedAndIdentified {
  id: number;
  name: string;
}

interface Product extends NamedAndIdentified {
  priceDollars: number;
}

interface Customer extends NamedAndIdentified {
  address: string;
}

Why not? Product.id and Customer.id are semantically different:

  • Customer.id might become a UUID
  • Product.name and Customer.name might evolve differently

Rule of thumb: If you can’t name it meaningfully, it’s probably premature abstraction.

Common Patterns

Base + Extensions

// Shared base
interface Vertebrate {
  weightGrams: number;
  color: string;
  isNocturnal: boolean;
}

// Specific extensions
interface Bird extends Vertebrate {
  wingspanCm: number;
}

interface Mammal extends Vertebrate {
  eatsGardenPlants: boolean;
}

Input/Output Types

// Full type
interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
}

// Input type derived
type CreateUserInput = Pick<User, 'email' | 'name'>;

// Or with Omit
type UpdateUserInput = Partial<Omit<User, 'id' | 'createdAt'>>;

Function Signatures

// ✅ Factor out common signatures
type HTTPFunction = (url: string, opts: Options) => Promise<Response>;

const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };

Pressure Resistance Protocol

1. “Copy-Paste Is Faster”

Pressure: “Just duplicate the type, it’s quicker”

Response: Technical debt accumulates. Types will drift apart.

Action: Take the time to use extends, Pick, or other derivations.

2. “The Types Are Different Enough”

Pressure: “They share fields by coincidence”

Response: Good point – verify they’re semantically the same first.

Action: Only factor out types that represent the same concept.

Red Flags – STOP and Reconsider

  • Copy-pasting interface fields
  • Multiple types with identical property subsets
  • Updating one type but forgetting another
  • Types named “…WithX” that duplicate base type

Common Rationalizations (All Invalid)

Excuse Reality
“It’s just a few fields” A few fields × many types = maintenance nightmare
“I’ll remember to update both” You won’t, or your teammates won’t
“The derivation is confusing” Less confusing than debugging drift

Quick Reference

// Adding fields
interface Extended extends Base { newField: T; }

// Selecting fields
type Subset = Pick<Full, 'a' | 'b'>;

// Removing fields  
type WithoutPassword = Omit<User, 'password'>;

// Making optional
type Updates = Partial<Options>;

// Getting return type
type Result = ReturnType<typeof myFunction>;

// Getting key union
type Keys = keyof MyInterface;

The Bottom Line

Derive types from a single source of truth.

Use TypeScript’s type operations (extends, Pick, Partial, keyof, typeof, mapped types) to express relationships between types. This keeps types in sync and reduces maintenance burden. But only apply DRY when types are semantically related, not just structurally similar.

Reference

Based on “Effective TypeScript” by Dan Vanderkam, Item 15: Use Type Operations and Generic Types to Avoid Repeating Yourself.