太长不看版:
satisfies
关键字,可以检验类型而不改变类型。相比:Type
类型定义,satisfies Type
会在检查类型的同时保留原本的隐式类型推导。而as
在任何时候都不能用于检查类型。
例子::Type
类型定义,或使用as
转换,导致信息丢失
颜色的表示,有两种方式,一种是RGB,如rgb(255, 255, 255)
。另一种是十六进制,如#FFFFFF
。前者在TypeScript中,可以用对象表示,后者用字符串表示。因此,Color类型定义如下。
ts
type RGBColor = {
red: number;
green: number;
blue: number;
};
type HexColor = string;
type Color = RGBColor | HexColor;
接着,定义画板的类型Canvas
。它有前景色foregroundColor
与背景色backgroundColor
两属性。
ts
type Canvas = {
// 前景色
foregroundColor: Color;
// 背景色
backgroundColor: Color;
}
编码的进程继续推进,接着我们定义了变量canvas1
。但留下了错误的代码。
ts
const canvas1 = {
backgroundColor: { red: 255, green: 0, bleu: 0 },
// ^
// 这里blue拼错了
foregroundColor: "#000000",
}
在编码时,把blue
误写成了bleu
。因为我们没有使用任何TypeScript的类型检查,无法在编码阶段发现错误。
加上类型定义:Canvas
,能检出错误
ts
const oneCanvas: Canvas = {
backgroundColor: { red: 255, green: 0, bleu: 0 },
foregroundColor: "#000000",
};
编译器提示:
Type '{ red: number; green: number; bleu: number; }' is not assignable to type 'Color'.
Object literal may only specify known properties, but 'bleu' does not exist in type 'RGBColor'. Did you mean to write 'blue'?ts(2322)
使用as Canvas
同样可以检出错误
ts
const oneCanvas = {
backgroundColor: { red: 255, green: 0, bleu: 0 },
foregroundColor: "#000000",
} as Canvas;
as
是对等号右侧的对象进行了类型转换,TS编译器抱怨转换无法完成。
Conversion of type '{ backgroundColor: { red: number; green: number; bleu: number; }; foregroundColor: string; }' to type 'Canvas' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
Types of property 'backgroundColor' are incompatible.
Type '{ red: number; green: number; bleu: number; }' is not comparable to type 'Color'.
Property 'blue' is missing in type '{ red: number; green: number; bleu: number; }' but required in type 'RGBColor'.ts(2352)
通过:Canvas
类型定义,或使用as
转换类型,发现并纠正blue
写成bleu
的笔误后。带来了一个新的问题:无法像as
转换前那样直接访问canvas1.backgroundColor.red
。
ts
const canvas1 = {
backgroundColor: { red: 255, green: 0, blue: 0 },
foregroundColor: "#000000",
} as Canvas;
const canvas2 = {
backgroundColor: { red: 255, green: 0, blue: 0 },
foregroundColor: "#000000",
};
console.log(canvas2.backgroundColor.red)
console.log(canvas1.backgroundColor.red);
// ^
// 提示字段不存在
报错信息如下
Property 'red' does not exist on type 'Color'.
Property 'red' does not exist on type 'string'.ts(2339)
报错信息直白明确。第二行提示red
字段是不会存在于string
(HexColor
)类型中的。
canvas1.backgroundColor
的类型是联合类型Color
。而red
字段只有当Color
是RGBColor
的时候才存在。
因此,需要收紧类型,才能访问到red
字段。
ts
if (typeof canvas1.backgroundColor === 'object') {
console.log(canvas1.backgroundColor.red);// 不报错
}
可canvas1是通过字符串字面量定义的,canvas1.backgroundColor.red
字段一定是存在的。typeof
做类型收紧是为了迁就类型声明或as
转换带来的类型丢失。
satisfies
关键字: 校验但不改变类型
对上面的代码中canvas1
部分进行修改
ts
const canvas1 = {
backgroundColor: { red: 255, green: 0, blue: 0 },
foregroundColor: "#000000",
} satisfies Canvas;
type TypeCanvas1 = typeof canvas1;
console.log(canvas1.backgroundColor.red); // 不报错
第五行对canvas1.backgroundColor.red
的访问通过了TypeScript的校验。
TypeCanvas1
来自于类型系统对等号右侧的对象字面量的推导。同时TypeCanvas1
能与Canvas
类型"兼容"。TypeCanvas1
是Canvas
的子类,所以能通过satisfies
的检验。
使用satisfies
的2种经典场景
函数传参时原地校验
一些Javascript的API,如JSON.stringify
,它们的参数类型会被定为any
。若不使用satisfies
,则需要定义一个变量并定义类型,才能对参数进行校验。而satisfies
可直接就地检验。
不用satisfies
ts
const canvas1: Canvas = {
backgroundColor: { red: 255, green: 0, blue: 0, XXYYAABB: 1 },
//////////////////////////////////////// ^ 会在这里报错
foregroundColor: "#000000",
};
JSON.stringify(canvas1);
要检出对象字面量的错误必须声明一个变量并给出:Canvas
类型定义。
使用satisfies
ts
JSON.stringify({
backgroundColor: { red: 255, green: 0, blue: 0, XXYYAABB: 1 },
//////////////////////////////////////// ^ 会在这里报错
foregroundColor: "#000000",
})
这个场景可以扩展到,调用fetch
, 设置searchParams
等一切参数被定义为any
但需要校验的场合。使用satisfies
校验类型能让代码更加健壮。
和as const
联用,保留常量类型的同时检验常量类型
Typescript 的类型系统,会隐式推断类型。as const
则会使这种类型推断更加精准,对于number
和string
的值,会被推断为一个常量而非number
或string
类型本身。对于数组则会推断为一个固定长度的元组。同时加上readonly
修饰符。
举一个as const
的例子。
ts
const a1 = [{ foo: 2 }]
const a2 = [{ foo: 2 }] as const;
a1
和a2
的类型都来自于隐式的类型系统推断。其中a1
的类型是{ foo: string }[]
,a2
的类型,因为加了as const
,类型会被推断为[{ foo: 1 }]
。
在等号左侧使用:Type
类型定义,会导致as const
推断出的类型丢失。而satisfies
只校验不转换类型能检查类型,同时保留as const
的类型推断结果。
ts
type Route = {
path: string;
};
const routes = {
HOME: { path: '/' },
USER: { path: '/user' },
REGISTER: { path: '/register' }
} as const satisfies Record<string, Route>;
function navigate(path: '/' | '/user') {
}
navigate(routes.HOME.path); // 不报错,参数的类型是`/`
navigate(routes.REGISTER.path); // 报错, 参数的类型是`/register`
在这段代码中,satisfies Record<string, Route>
校验了routes
对象各个字段的值。同时as const
推断出的类型信息被保留。第14行校验能通过,而15行则被TypeScript检出错误。
深入研究::Type
类型定义、as
、satisfies
的底层逻辑
标题中说,satisfies
检验类型,比as
更准确。甚至在上文的例子中,都尽量避免使用as
。现在到了揭晓原因的时刻。
下面的代码,wrong1
和wrong2
竟然都能通过类型系统的检查。不符合预期。
ts
const wrong1 = {} as Route;
const wrong2 = { path: 1, OTHERS: 2 } as Route;
原因是:as
被设计出来,就不是用来做类型检查的。它的功能是做类型转换。检查出的错误是类型系统认为不能转换产生的错误。
任何时候都不要用as去做类型检查
类型检查只有satisfies
和:Type
定义才能做到
TypeA as TypeB
,当B可以作为A的子类型时,类型系统的检查就能通过。
而TypeA satisfies TypeB
或const foo:TypeB = a
正好相反。只有类型B能作为类型A的父类时,才能通过类型系统的检验。
一个形象的比喻:
as让类型站在地板上向上跳跃,satisfies
检查地板是否稳固
:Type
类型定义,与satisfies
的唯一区别:前者会显式地声明类型,后者只检查类型同时保留原有隐式推断的类型。和as const
配合时也能尽可能多的保留类型。