TypeScript 的 satisfies 运算符已经推出一段时间了,但它似乎仍然是一个可以用来澄清的混乱来源。
可以把 satisfies 看作是将类型赋给值的另一种方式。
在我们深入研究之前,让我们回顾一下如何赋值类型。
首先,有一个不起眼的“冒号注解”(我们使用这个听起来有点像医学术语的名字,因为这个概念在 TS 文档中并没有真正给出一个名字)。
:表示“这个变量总是这个类型”:
const obj: Record<string, string> = {};
obj.id = "123";
当你使用冒号注解时,你声明了变量是该类型。
这意味着你赋值给变量的东西必须是该类型:
// Type 'number' is not assignable to type 'string'.
const str: string = 123;
给变量赋予的类型可能比你最初赋予的类型更宽。
let id: string | number = "123";
if (typeof numericId !== "undefined") {
id = numericId;
}
当你想要有一个默认值,而这个默认值以后可能会被重新赋值时,这个方法非常有用。
但是冒号注释有一个缺点。
当使用冒号时,类型优先于值。
换句话说,如果你声明的类型比你想要的宽,你就被这个宽的类型困住了。
例如,在下面的代码片段中,你没有在routes对象上获得 autocomplete:
const routes: Record<string, {}> = {
"/": {},
"/users": {},
"/admin/users": {},
};
// No error!
routes.awdkjanwdkjn;
这就是 satisfies
被设计用来解决的问题。
这意味着它能推断出最窄的可能类型,而不是你指定的更宽的类型:
const routes = {
"/": {},
"/users": {},
"/admin/users": {},
} satisfies Record<string, {}>;
// Property 'awdkjanwdkjn' does not exist on type
// '{ "/": {}; "/users": {}; "/admin/users": {}; }'
routes.awdkjanwdkjn;
satisfies
还能防止在配置对象中指定错误的内容。
因此,satisfies
和 冒号注解 同样安全。
const routes = {
// Type 'null' is not assignable to type '{}'
"/": null,
} satisfies Record<string, {}>;
另一种为变量赋值的方式是使用 “ as
” 注解。
与 satisfies
和冒号注解不同,使用 “ as
” 注解可以让你对 TypeScript 说谎。
在这个例子中,你在IDE中看不到错误,但它会在运行时崩溃:
type User = {
id: string;
name: {
first: string;
last: string;
};
};
const user = {} as User;
// No error! But this will break at runtime
user.name.first;
这些谎言有一些限制——你可以给对象添加属性,但是你不能在基本类型之间转换。
例如,你不能强制 TypeScript 将字符串转换为数字......
除非你用那个怪异的“as-as”:
// Conversion of type 'string' to type 'number'
// may be a mistake because neither type
// sufficiently overlaps with the other.
const str = "my-string" as number;
const str2 = "my-string" as unknown as number;
下面是 as
的合法用法,它用于将对象转换为尚未构造的已知类型:
type User = {
id: string;
name: string;
};
// The user hasn't been constructed yet, so we need
// to use 'as' here
const userToBeBuilt = {} as User;
(["name", "id"] as const).forEach((key) => {
// Assigning to a dynamic key!
userToBeBuilt[key] = "default";
});
警告:如果你使用 as
作为注解变量的默认方式,那几乎肯定是错误的!
下面的代码看起来很安全,但是一旦你给 User
类型添加了另一个属性, defaultUser
就过时了,而且它不会显示错误!
type User = {
id: string;
name: string;
};
const defaultUser = {
id: "123",
name: "Matt",
} as User;
还有一种方法可以给变量赋类型:
什么都不加。
这不是打字错误!
TypeScript 在推断变量类型方面做得很好。
事实上,大多数情况下,你根本不需要输入变量:
const routes = {
"/": {},
"/users": {},
"/admin/users": {},
};
// OK!
routes["/"];
// Property 'awdahwdbjhbawd' does not exist on type
// { "/": {}; "/users": {}; "/admin/users": {}; }
routes["awdahwdbjhbawd"];
总结一下,我们有四种方法来给变量赋类型:
-
冒号注解
-
satisfies
-
as
注解 -
不注解,让TS推断
由于有不同的方法来做类似的事情,它可能会让人们对何时使用何种方法感到有点困惑。
简单的用例是 satisfies
最适用的:
type User = {
id: string;
name: string;
};
const defaultUser = {
id: "123",
name: "Matt",
} satisfies User;
但是大多数时候,当你想给变量赋一个类型时,你可能希望这个类型更宽。
如果这个例子使用的是 satisfies
,那么就不能把 numericId
赋给 id
:
// colon annotation
let id: string | number = "123";
if (typeof numericId !== "undefined") {
id = numericId;
}
// satisfies
let id = "123" satisfies string | number;
if (typeof numericId !== "undefined") {
// Type 'number' is not assignable to type 'string'.
id = numericId;
}
经验法则是,你应该只在以下两种特定情况下使用satisfies:
-
你想要的是变量的精确类型,而不是宽泛类型。
-
这个类型足够复杂,你需要确保你没有把它搞砸。
欢迎关注公众号:文本魔术,了解更多