一键导入
dry-types
Use when duplicating type definitions. Use when interfaces share common fields. Use when types can be derived from other types.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when duplicating type definitions. Use when interfaces share common fields. Use when types can be derived from other types.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Use when defining global types. Use when augmenting window. Use when typing environment variables. Use when working with build-time constants. Use when configuring type definitions.
Use when migrating JavaScript to TypeScript. Use when gradually adopting TypeScript. Use when working with mixed codebases. Use when converting large projects. Use when teams are learning TypeScript.
Use when writing asynchronous code. Use when tempted to use callbacks. Use when composing multiple async operations.
Use when creating types from example data. Use when types don't match all cases. Use when API responses vary.
Use when writing type annotations on variables. Use when TypeScript can infer the type. Use when code feels cluttered with types.
Use when defining array-like types. Use when tempted to use number as index type. Use when understanding array keys.
| name | dry-types |
| description | Use when duplicating type definitions. Use when interfaces share common fields. Use when types can be derived from other types. |
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.
NEVER copy-paste type definitions. Derive types from a single source of truth.
Remember:
extends for adding fieldsPick<T, K> for selecting fieldsPartial<T> for making fields optionalkeyof for getting key typestypeof for deriving types from valuesIf 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.
extends to Add Fields// ✅ Derive from base type
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate extends Person {
birth: Date;
}
Pick to Select Fieldsinterface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
// ✅ Select only the fields you need
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
Partial for Optional Versionsinterface Options {
width: number;
height: number;
color: string;
}
class UIWidget {
constructor(init: Options) { /* ... */ }
// ✅ All fields optional for updates
update(options: Partial<Options>) { /* ... */ }
}
keyof for Key Typestype OptionsKeys = keyof Options;
// ^? type OptionsKeys = "width" | "height" | "color"
typeof to Derive from Valuesconst DEFAULTS = {
width: 640,
height: 480,
color: '#00FF00',
};
// ✅ Type derived from value
type Options = typeof DEFAULTS;
// ^? type Options = { width: number; height: number; color: string; }
| 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> |
// Create optional version manually
type OptionsUpdate = {
[K in keyof Options]?: Options[K]
};
// Equivalent to Partial<Options>
interface SaveAction { type: 'save'; /* ... */ }
interface LoadAction { type: 'load'; /* ... */ }
type Action = SaveAction | LoadAction;
// ✅ Extract discriminant type
type ActionType = Action['type'];
// ^? type ActionType = "save" | "load"
function getUserInfo(userId: string) {
return {
userId,
name,
age,
// ... many fields
};
}
// ✅ Derive type from function
type UserInfo = ReturnType<typeof getUserInfo>;
asinterface ShortToLong {
q: 'search';
n: 'numberOfResults';
}
// Invert the mapping
type LongToShort = {
[K in keyof ShortToLong as ShortToLong[K]]: K
};
// ^? type LongToShort = { search: "q"; numberOfResults: "n"; }
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; }
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:
Rule of thumb: If you can't name it meaningfully, it's probably premature abstraction.
// Shared base
interface Vertebrate {
weightGrams: number;
color: string;
isNocturnal: boolean;
}
// Specific extensions
interface Bird extends Vertebrate {
wingspanCm: number;
}
interface Mammal extends Vertebrate {
eatsGardenPlants: boolean;
}
// 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'>>;
// ✅ Factor out common signatures
type HTTPFunction = (url: string, opts: Options) => Promise<Response>;
const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };
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.
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.
| 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 |
// 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;
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.
Based on "Effective TypeScript" by Dan Vanderkam, Item 15: Use Type Operations and Generic Types to Avoid Repeating Yourself.