使用 Partial 设置可变的默认参数
当一个函数中有默认参数的时候,若此参数是一个对象,那么我们需要对这个对象的每一个field
都设置类型。而且当新增 field
的时候,还需要在多个地方进行新增。
type Options = {
from: string;
to: string;
};
const defaultOptions: Options = {
from: "./src",
to: "./dest",
};
type PartialOptions = {
from?: string;
to?: string;
};
function copy(options: PartialOptions) {
const allOptions = { ...defaultOptions, ...options};
// todo: do something
}
遇到这种情况的时候,我们可以使用 Partial
来进行精简:
const defaultOptions = {
from: "./src",
to: "./dest",
overwrite: true,
};
function copy(options: Partial<typeof defaultOptions>) {
const allOptions = { ...defaultOptions, ...options};
// todo: do something
}
这样,就可以在保证类型安全的前提下进行随意的扩展。这时我们只需要关注唯一的一个数据源 defaultOptions
,这是唯一实际出现在运行时的对象。这样书写能减轻 TS 的侵入性。
Tuple to union
在 const 上下文、typeof 操作符以及索引存取符的作用下,可以将 tuple 转化成 union。
const categories = [
"beginner",
"intermediate",
"advanced",
] as const;
// "beginner" | "intermediate" | "advanced"
type Category = (typeof categories)[number]
同样的,这时候我们只需要维护一份数据,可以在 categories 中进行增减操作。
用类型进行逻辑操作
假设我们有多种游戏类型,每种类型都有一个 kind field
来标识游戏类型,我们可以得到以下的表示方式:
// 类型公共的 field
type ToyBase = {
name: string;
price: number;
quantity: number;
minimumAge: number;
}
// 具体的类型
type BoardGame = ToyBase & {
kind: 'boardgame';
players: number;
}
type Puzzle = ToyBase & {
kind: 'puzzle';
pieces: number;
}
type Doll = ToyBase & {
kind: 'doll';
material: 'plastic' | 'plush'
}
// 玩具的类型
type Toy = BoardGame | Puzzle | Doll
我们可以借助 kind 属性来进行类型收窄判断
function printToy(toy: Toy) {
switch(toy.kind) {
case "boardgame":
// todo
break;
case "puzzle":
// todo
break;
case "doll":
// todo
break;
default:
console.log(toy);
}
}
如果需要更为详尽的类型分类,我们会有以下新的类型:
type ToyKind = 'boardgame' | 'puzzle' | 'doll'
type GroupedToys = {
boardgame: BoardGame[];
puzzle: Puzzle[];
doll: Doll[];
}
这时候,如果我们想新增一个游戏类型,比如说 VideoGame
:
type VideoGame = ToyBase & {
kind: 'videogame';
system: 'Switch' | 'Wii' | 'PS5'
}
这时候,我们需要同时对 Toy
、ToyKind
和 GroupedToys
来进行修改。这时候我们可以使用 TS 内置的函数来降低维护成本。
// ToyKind 可以使用另一种表示方式
type ToyKind = Toy['kind'];
下一步,我们可以构造一个类型推导函数来对具体类型进行推导:
type GetKind<Group, Kind> = Extract<Group, { kind: Kind }>
// 这时,GroupedToys 类型就可以自行进行推导
type GroupedToys = {
[Kind in ToyKind]: GetKind<Toy, Kind>[]
}
// 上面的写法就等效于
type GroupedToys = {
boardgame: BoardGame[];
puzzle: Puzzle[];
doll: Doll[];
}
我们也可以使用 as 操作符来对属性进行修改:
type GroupedToys = {
[Kind in ToyKind as `${Kind}s`]: GetKind<Toy, Kind>[]
};
// 等效于
type GroupedToys = {
boardgames: BoardGame[];
puzzles: Puzzle[];
dolls: Doll[];
}
总结
通过上面的例子,我们可以总结出 ToyKind
、GroupedToys
都属于衍生类型,对于衍生类型我们需要尽可能的使用推导来产生,这样就可以减少我们需要维护的类型数量。通常,以下的几个要点可以帮助我们降低类型维护的复杂度:
- 使用模型来创建数据或者从已有类型中进行推导
- 定义衍生类型(mapped types 或者 Partials)
- 定义行为(条件判断)