likes
comments
collection
share

TypeScript中的satisfies关键字,比as更准确,比`:Type`类型定义更简洁

作者站长头像
站长
· 阅读数 4

太长不看版:satisfies关键字,可以检验类型而不改变类型。相比:Type类型定义,satisfies Type会在检查类型的同时保留原本的隐式类型推导。而as任何时候都不能用于检查类型

例子::Type类型定义,或使用as转换,导致信息丢失

颜色的表示,有两种方式,一种是RGB,如rgb(255, 255, 255)。另一种是十六进制,如#FFFFFF。前者在TypeScript中,可以用对象表示,后者用字符串表示。因此,Color类型定义如下。

type RGBColor = {
  red: number;
  green: number;
  blue: number;
};

type HexColor = string;
type Color = RGBColor | HexColor;

接着,定义画板的类型Canvas。它有前景色foregroundColor与背景色backgroundColor两属性。

type Canvas = {
      // 前景色
      foregroundColor: Color;
      // 背景色
      backgroundColor: Color;
}

编码的进程继续推进,接着我们定义了变量canvas1。但留下了错误的代码。

const canvas1 = {
  backgroundColor: { red: 255, green: 0, bleu: 0 },
  //                                      ^
  //                                      这里blue拼错了
  foregroundColor: "#000000",
}

在编码时,把blue误写成了bleu。因为我们没有使用任何TypeScript的类型检查,无法在编码阶段发现错误。

加上类型定义:Canvas,能检出错误

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同样可以检出错误

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

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字段只有当ColorRGBColor的时候才存在。

因此,需要收紧类型,才能访问到red字段。

if (typeof canvas1.backgroundColor === 'object') {
  console.log(canvas1.backgroundColor.red);// 不报错
}

可canvas1是通过字符串字面量定义的,canvas1.backgroundColor.red字段一定是存在的。typeof做类型收紧是为了迁就类型声明或as转换带来的类型丢失。

satisfies关键字: 校验但不改变类型

对上面的代码中canvas1部分进行修改

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类型"兼容"。TypeCanvas1Canvas的子类,所以能通过satisfies的检验。

使用satisfies的2种经典场景

函数传参时原地校验

一些Javascript的API,如JSON.stringify,它们的参数类型会被定为any。若不使用satisfies,则需要定义一个变量并定义类型,才能对参数进行校验。而satisfies可直接就地检验。

不用satisfies

const canvas1: Canvas = {
  backgroundColor: { red: 255, green: 0, blue: 0, XXYYAABB: 1 },
  ////////////////////////////////////////           ^ 会在这里报错
  foregroundColor: "#000000",
};

JSON.stringify(canvas1);

要检出对象字面量的错误必须声明一个变量并给出:Canvas类型定义。

使用satisfies

JSON.stringify({
  backgroundColor: { red: 255, green: 0, blue: 0, XXYYAABB: 1 },
  ////////////////////////////////////////           ^ 会在这里报错
  foregroundColor: "#000000",
})

这个场景可以扩展到,调用fetch, 设置searchParams等一切参数被定义为any但需要校验的场合。使用satisfies校验类型能让代码更加健壮。

as const联用,保留常量类型的同时检验常量类型

Typescript 的类型系统,会隐式推断类型。as const则会使这种类型推断更加精准,对于numberstring的值,会被推断为一个常量而非numberstring类型本身。对于数组则会推断为一个固定长度的元组。同时加上readonly修饰符。

举一个as const的例子。

const a1 = [{ foo: 2 }]

const a2 = [{ foo: 2 }] as const;

a1a2的类型都来自于隐式的类型系统推断。其中a1的类型是{ foo: string }[]a2的类型,因为加了as const,类型会被推断为[{ foo: 1 }]

在等号左侧使用:Type类型定义,会导致as const推断出的类型丢失。而satisfies只校验不转换类型能检查类型,同时保留as const的类型推断结果。

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类型定义、assatisfies的底层逻辑

标题中说,satisfies检验类型,比as更准确。甚至在上文的例子中,都尽量避免使用as。现在到了揭晓原因的时刻。

下面的代码,wrong1wrong2竟然都能通过类型系统的检查。不符合预期。

const wrong1 = {} as Route;
const wrong2 = { path: 1, OTHERS: 2 } as Route;

原因是:as被设计出来,就不是用来做类型检查的。它的功能是做类型转换。检查出的错误是类型系统认为不能转换产生的错误。

任何时候都不要用as去做类型检查

类型检查只有satisfies:Type定义才能做到

TypeA as TypeB,当B可以作为A的子类型时,类型系统的检查就能通过。

TypeA satisfies TypeBconst foo:TypeB = a正好相反。只有类型B能作为类型A的父类时,才能通过类型系统的检验。

一个形象的比喻:

as让类型站在地板上向上跳跃,satisfies检查地板是否稳固

:Type类型定义,与satisfies的唯一区别:前者会显式地声明类型,后者只检查类型同时保留原有隐式推断的类型。和as const配合时也能尽可能多的保留类型。

转载自:https://juejin.cn/post/7344573755644330038
评论
请登录