- The DRY (don't repeat yourself) principle applies to types as much as it applies to logic.
- Name types rather than repeating them. Use
extends
to avoid repeating fields in interfaces.
[role="less_space pagebreak-before"]
- Build an understanding of the tools provided by TypeScript to map between types. These include
keyof
,typeof
, indexing, and mapped types. - Generic types are the equivalent of functions for types. Use them to map between types instead of repeating type-level operations.
- Familiarize yourself with generic types defined in the standard library, such as
Pick
,Partial
, andReturnType
. - Avoid over-application of DRY: make sure the properties and types you're sharing are really the same thing.
console.log(
'Cylinder r=1 × h=1',
'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 1 * 1,
'Volume:', 3.14159 * 1 * 1 * 1
);
console.log(
'Cylinder r=1 × h=2',
'Surface area:', 6.283185 * 1 * 1 + 6.283185 * 2 * 1,
'Volume:', 3.14159 * 1 * 2 * 1
);
console.log(
'Cylinder r=2 × h=1',
'Surface area:', 6.283185 * 2 * 1 + 6.283185 * 2 * 1,
'Volume:', 3.14159 * 2 * 2 * 1
);
type CylinderFn = (r: number, h: number) => number;
const surfaceArea: CylinderFn = (r, h) => 2 * Math.PI * r * (r + h);
const volume: CylinderFn = (r, h) => Math.PI * r * r * h;
for (const [r, h] of [[1, 1], [1, 2], [2, 1]]) {
console.log(
`Cylinder r=${r} × h=${h}`,
`Surface area: ${surfaceArea(r, h)}`,
`Volume: ${volume(r, h)}`);
}
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate {
firstName: string;
lastName: string;
birth: Date;
}
function distance(a: {x: number, y: number}, b: {x: number, y: number}) {
return Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2);
}
interface Point2D {
x: number;
y: number;
}
function distance(a: Point2D, b: Point2D) { /* ... */ }
function get(url: string, opts: Options): Promise<Response> { /* ... */ }
function post(url: string, opts: Options): Promise<Response> { /* ... */ }
type HTTPFunction = (url: string, opts: Options) => Promise<Response>;
const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };
interface Person {
firstName: string;
lastName: string;
}
interface PersonWithBirthDate extends Person {
birth: Date;
}
interface Bird {
wingspanCm: number;
weightGrams: number;
color: string;
isNocturnal: boolean;
}
interface Mammal {
weightGrams: number;
color: string;
isNocturnal: boolean;
eatsGardenPlants: boolean;
}
interface Vertebrate {
weightGrams: number;
color: string;
isNocturnal: boolean;
}
interface Bird extends Vertebrate {
wingspanCm: number;
}
interface Mammal extends Vertebrate {
eatsGardenPlants: boolean;
}
type PersonWithBirthDate = Person & { birth: Date };
interface State {
userId: string;
pageTitle: string;
recentFiles: string[];
pageContents: string;
}
interface TopNavState {
userId: string;
pageTitle: string;
recentFiles: string[];
// omits pageContents
}
interface TopNavState {
userId: State['userId'];
pageTitle: State['pageTitle'];
recentFiles: State['recentFiles'];
};
type TopNavState = {
[K in 'userId' | 'pageTitle' | 'recentFiles']: State[K]
};
type TopNavState = Pick<State, 'userId' | 'pageTitle' | 'recentFiles'>;
interface SaveAction {
type: 'save';
// ...
}
interface LoadAction {
type: 'load';
// ...
}
type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load'; // Repeated types!
type ActionType = Action['type'];
// ^? type ActionType = "save" | "load"
type ActionRecord = Pick<Action, 'type'>;
// ^? type ActionRecord = { type: "save" | "load"; }
interface Options {
width: number;
height: number;
color: string;
label: string;
}
interface OptionsUpdate {
width?: number;
height?: number;
color?: string;
label?: string;
}
class UIWidget {
constructor(init: Options) { /* ... */ }
update(options: OptionsUpdate) { /* ... */ }
}
type OptionsUpdate = {[k in keyof Options]?: Options[k]};
type OptionsKeys = keyof Options;
// ^? type OptionsKeys = keyof Options
// (equivalent to "width" | "height" | "color" | "label")
class UIWidget {
constructor(init: Options) { /* ... */ }
update(options: Partial<Options>) { /* ... */ }
}
interface ShortToLong {
q: 'search';
n: 'numberOfResults';
}
type LongToShort = { [k in keyof ShortToLong as ShortToLong[k]]: k };
// ^? type LongToShort = { search: "q"; numberOfResults: "n"; }
interface Customer {
/** How the customer would like to be addressed. */
title?: string;
/** Complete name as entered in the system. */
readonly name: string;
}
type PickTitle = Pick<Customer, 'title'>;
// ^? type PickTitle = { title?: string; }
type PickName = Pick<Customer, 'name'>;
// ^? type PickName = { readonly name: string; }
type ManualName = { [K in 'name']: Customer[K]; };
// ^? type ManualName = { name: string; }
type PartialNumber = Partial<number>;
// ^? type PartialNumber = number
const INIT_OPTIONS = {
width: 640,
height: 480,
color: '#00FF00',
label: 'VGA',
};
interface Options {
width: number;
height: number;
color: string;
label: string;
}
type Options = typeof INIT_OPTIONS;
function getUserInfo(userId: string) {
// ...
return {
userId,
name,
age,
height,
weight,
favoriteColor,
};
}
// Return type inferred as { userId: string; name: string; age: number, ... }
type UserInfo = ReturnType<typeof getUserInfo>;
interface Product {
id: number;
name: string;
priceDollars: number;
}
interface Customer {
id: number;
name: string;
address: string;
}
// Don't do this!
interface NamedAndIdentified {
id: number;
name: string;
}
interface Product extends NamedAndIdentified {
priceDollars: number;
}
interface Customer extends NamedAndIdentified {
address: string;
}