| name | dry-types |
| description | Use when duplicating type definitions. Use when interfaces share common fields. Use when types can be derived from other types. |
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:
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate {
firstName: string;
lastName: string;
birth: Date;
}
What if you add middleName to Person? Now they're out of sync.
Basic Techniques
Use extends to Add Fields
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;
}
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) { }
update(options: Partial<Options>) { }
}
Use keyof for Key Types
type OptionsKeys = keyof Options;
Use typeof to Derive from Values
const DEFAULTS = {
width: 640,
height: 480,
color: '#00FF00',
};
type Options = typeof DEFAULTS;
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
type OptionsUpdate = {
[K in keyof Options]?: Options[K]
};
Indexing into Union Types
interface SaveAction { type: 'save'; }
interface LoadAction { type: 'load'; }
type Action = SaveAction | LoadAction;
type ActionType = Action['type'];
ReturnType for Function Results
function getUserInfo(userId: string) {
return {
userId,
name,
age,
};
}
type UserInfo = ReturnType<typeof getUserInfo>;
Key Remapping with as
interface ShortToLong {
q: 'search';
n: 'numberOfResults';
}
type LongToShort = {
[K in keyof ShortToLong as ShortToLong[K]]: K
};
Homomorphic Mapped Types
Mapped types preserve modifiers when using keyof:
interface Customer {
title?: string;
readonly name: string;
}
type PickTitle = Pick<Customer, 'title'>;
type PickName = Pick<Customer, 'name'>;
When NOT to Apply DRY
Don't factor out types that are only coincidentally similar:
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
interface Vertebrate {
weightGrams: number;
color: string;
isNocturnal: boolean;
}
interface Bird extends Vertebrate {
wingspanCm: number;
}
interface Mammal extends Vertebrate {
eatsGardenPlants: boolean;
}
Input/Output Types
interface User {
id: string;
email: string;
name: string;
createdAt: Date;
}
type CreateUserInput = Pick<User, 'email' | 'name'>;
type UpdateUserInput = Partial<Omit<User, 'id' | 'createdAt'>>;
Function 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
interface Extended extends Base { newField: T; }
type Subset = Pick<Full, 'a' | 'b'>;
type WithoutPassword = Omit<User, 'password'>;
type Updates = Partial<Options>;
type Result = ReturnType<typeof myFunction>;
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.