

在处理可能以多种形式成立的条件时,我们通常会使用 switch case 语句,一个结合 typescript 的常见例子是处理枚举的各个成员值:
declare enum color {
red,
yellow,
blue,
}
declare let color: color;
switch (color) {
case color.red:
// do something
break;
case color.yellow:
// do something
break;
case color.blue:
// do something
break;
default:
break;
}
color.pink
没有被处理,那使用粉色的在逃公主们很可能就直接卸载你的应用了。
declare enum color {
red,
yellow,
blue,
pink,
}
declare let color: color;
switch (color) {
case color.red:
// do something
break;
case color.yellow:
// do something
break;
case color.blue:
// do something
break;
default:
// 不能将类型“color”分配给类型“never”。
let exhaustivecheck: never = color;
break;
}
throw new error('unhandled color')
,使用 never 类型进行检查,能够在开发阶段(最晚也就是构建阶段)就提前警示可能的错误。
let exhaustivecheck: never = color;
实现原理:
在 switch case 或 if else 语句中,随着变量的类型成员不断被对应的分支认领,其类型会在后续的代码控制流中被移除,当所有类型成员都被移除时,typescript 会用 never 类型描述其类型,而 never 类型的变量无法被赋给除了 never 类型以外的值。
因此,在这个例子中,如果有遗漏的类型分支,那么 color 的类型就不会被描述为 never,就会导致类型不兼容的错误。

使用互斥类型替代联合类型
我们经常使用联合类型描述一组相近的实体类型,比如我们希望一个变量要么符合游客 visitor 类型,要么符合注册用户 registered 类型,不允许同时符合(即同时拥有 referer 与 email 这两个属性)。一般我们会想到使用联合类型 user:
interface visitor {
referer: string;
}
interface registered {
email: string;
}
type user = visitor | registered;
但这其实是个误区,因为联合类型不会约束「不能同时符合」这一点:
const user: user = {
referer: 'www.google.com',
email: 'linbudu@qq.com',
};
这可能会导致后续的代码处理出现问题,比如可能有判断 user.email
存在就认为它是已注册用户的逻辑。
为了表示「不能同时拥有」,我们可以使用互斥类型 xor:
type xoruser = xor<visitor, registered>;
// 属性“email”的类型不兼容。
const user1: xoruser = {
referer: 'www.google.com',
};
user1.email; // undefined
user1.email = 'linbudu@qq.com'; // x
// 属性“email”的类型不兼容。
const user2: xoruser = {
referer: 'www.google.com',
email: 'linbudu@qq.com',
};
xor 的两个类型参数表示这两个类型互斥,因此你也可以实现「要么同时存在,要么同时不存在」的属性绑定,只需要为其中一个参数指定 {} 类型即可。
interface registered {
email: string;
registertime: number;
level: number;
}
type xorstruct = xor<{}, registered>;
const val1: xorstruct = {}; // √
// x
const val2: xorstruct = {
email: 'linbudu@qq.com',
};
// √
const val3: xorstruct = {
email: 'linbudu@qq.com',
registertime: date.now(),
level: 9999,
};
公式:
type without<t, u> = { [p in exclude<keyof t, keyof u>]?: never };
type xor<t, u> = (without<t, u> & u) | (without<u, t> & t);
使用:
type xortype = xor<type1, type2>;
// 实现三个类型彼此互斥
type xortype2 = xor<xortype, type3>;
实现原理:
-
通过 exclude ,即差集类型,声明两个类型相对的互斥结构。 -
通过 never 类型来禁止一个属性存在。

保留联合类型的提示
在开发组件时,一个常见的场景是某个属性既可以有一组预设的值,又可以是任意的同类型值,如:
type size = 'mini' | 'middle' | 'large';
let size1: size = 'mini';
// 不能将类型“"200px"”分配给类型“size”。
let size2: size = '200px';
这种时候怎么描述类型就有点矛盾了,我又想提供字面量联合类型的提示,又想支持任意的字符串类型,应该怎么做?如果直接 size | string,那么 size 中的联合类型会被合并进 string,导致最后类型描述为 string 类型。
这里有个小 trick,可以这么做:
type presetsize = 'mini' | 'middle' | 'large';
type size = presetsize | (string & {});
let size1: size = 'mini';
let size2: size = '200px';
公式:
请直接复制这个工具类型:
type smartliteral<t extends keyof any> = t | (string & {});
使用:
type smartliteral<t extends keyof any> = t | (string & {});
type presetsize = 'mini' | 'middle' | 'large';
type size = smartliteral<presetsize>;
let size1: size = 'mini';
let size2: size = '200px';
string & {}
这个类型等价于 string 类型,可参考 typescript 4.8 版本中 - 交叉类型与联合类型的类型收窄增强 中的介绍,而与空对象进行交叉类型,又确保了它在联合类型中不会被视为其它字面量类型的父类型,从而避免了类型合并。
satisfies 关键字
satisfies 关键字引入于 typescript 4.9 版本,用于实现「使用类型约束值,但仍然使用值本身推导的类型」的效果。
type colors = 'red' | 'green' | 'blue';
type rgb = [number, number, number];
type palette = record<colors, string | rgb>;
const palette = {
red: [255, 0, 0],
green: '#00ff00',
blue: [0, 0, 255],
} satisfies palette;
// string
palette.green.startswith('#'); // √
// [number, number, number]
palette.red.find(() => true); // √
// [number, number, number];
palette.blue.entries(); // √
在这个例子中,我们要求变量 palette 的类型满足 palette 结构,同时没有像类型断言或类型标注的效果一样(标注为 palette 类型,或断言到 palette 类型),将变量类型修改为了 palette 类型,而是继续保留了其原始推导出的字面量类型结构。
关于 satisfies 、类型标注、类型断言与隐式类型推导的差异,请阅读:typescript 4.9 beta: satisfies 操作符。

当你希望获得一组规律固定,可由排列组合得到的联合类型时,可以使用模板字符串类型的插槽组合特性:
type software = 'wechat' | 'alipay' | 'lolm';
type platform = 'android' | 'ios' | 'harmonyos';
type versiontag = 'debug' | 'stable' | 'nightly';
type products = `${software}-${platform}-${versiontag}`
使用重映射快速修改接口
name | age | job
的接口类型,你希望能基于其派生出一个属性为
updatedname | updatedage | updatedjob
的接口类型,这样一来在原接口属性发生变化时,你无需进行手动处理。interface user {
name: string;
age: number;
job: string;
}
// {
// updatedname: string;
// updatedage: number;
// updatedjob: string;
// }
type updateduser = {
[k in keyof user as `updated${capitalize<k>}`]: user[k];
};
公式:
type derivedstruct<struct extends object> = {
[k in keyof struct as `updated${capitalize<k & string>}`]: struct[k];
};
type updateduser2 = derivedstruct<user>;
实现原理:
-
重映射,索引类型签名中 as 开始的部分,能够在索引类型映射时将其修改为一个新的字符串类型值。 -
capitalize 工具类型,随模板字符串类型一同引入的内置工具类型,功能是将此字符串类型的首字母大写。 -
k & string
,通过交叉类型的结果同时满足其类型成员的定义,确保类型符合 capitalize 的泛型类型约束。
in deep:
如果接口中只有一部分属性需要进行处理,应该怎么办?当然可以实现一个 derivedstructfromproperties ,然后再开放一个参数来确定需要处理的属性,但这样又变成需要手动处理了。更好的方式是拆分你的接口:
interface userdetail {}
interface userrelation {}
interface userlevel {}
interface user extends userdetail, userrelation, userlevel {}
interface updateduser
extends derivedstruct<userdetail>,
userrelation,
userlevel {}
其中 userdetail 即为需要处理的属性集合。
提取类型
某些时候我们可能会遇到这么个情况,某个三方的 npm 包,导出了类型 a,其中引用了类型 b(但没有导出),而现在我们需要的就是类型 b。
这种时候我们自己使用 infer 关键字来从类型 a 提取类型 b,常见的有这么几种:
-
提取数组类型的元素类型
type arrayelementtype<t extends any[]> = t extends (infer u)[] ? u : never;
type userlist = array<{
id: number;
name: string;
age: number;
}>;
// {
// id: number;
// name: string;
// age: number;
// }
type user = arrayelementtype<userlist>;
-
提取 promise 的值类型
type queryuserresponse = promise<{
id: string;
name: string;
email: string;
}>;
// {
// id: string;
// name: string;
// email: string;
// }
type user = awaited<queryuserresponse>;
awaited 是 typescript 内置的工具类型,可以用于提取一个 promise resolve 的值类型。
-
提取入参类型与返回值类型
interface user {}
interface updateduser extends user {}
// 仅导出了函数
export function updateuser(input: user): updateduser {}
// user
type inputuser = parameters<typeof updateuser>[0];
// updateduser
type outputuser = returntype<typeof updateuser>;
parameters、returntype 都是内置的工具类型,分别用于提取函数的参数类型与返回类型值。
携带泛型的 react 组件
在 list / waterfall 这一类组件中,常见的设计是由 datasource 属性接受数据源,再由 renderitem 属性负责遍历 datasource 生成内部子元素,这也就意味着应该让 datasource 的类型能够传递到 renderitem 中,如:
import react from 'react';
function foo() {
return (
<scroller
datasource={[5, 9, 9]}
// item 为 number 类型
renderitem={(item) => {
return <div>{item.tofixed()}</div>;
}}
/>
);
}
要实现以上的效果,你可以使用携带泛型的接口结构,声明组件类型:
import react from 'react';
interface scrollerprops<tdata = any> {
datasource: tdata[];
renderitem?: (item: tdata) => react.reactelement;
}
export interface generictypingwrapper {
<tinputdata = any>(props: scrollerprops<tinputdata>): react.reactelement;
}
const list: generictypingwrapper = ({ datasource }) => {
return <></>;
};
如果你希望在组件编写时使用 react.fc
,那么可以在导出组件时使用类型断言修正类型:
const scroller: react.fc<scrollerprops> = ({ datasource }) => {
return <></>;
};
export default scroller as generictypingwrapper;
结语
本文总结了在业务代码中实用的多种 typescript 技巧,涵盖了使用 never 类型检查 switch case 语句以确保枚举或联合类型分支处理的完整性,用互斥类型替代联合类型来避免类型冲突,保留联合类型提示的方法,satisfies 关键字的运用,模板字符串类型的排列组合特性,利用重映射快速修改接口,提取特定类型的方式,以及在 react 组件中实现携带泛型等内容。每个技巧都配有详细的代码示例和原理讲解,有助于开发者在实际编程中更高效、准确地使用 typescript 。
发表评论