创建了一个 “重学TypeScript” 的微信群,想加群的小伙伴,加我微信 "semlinker",备注重学TS。已出 TS 系列文章 41 篇。
一、类型推断与上下文
TypeScript 不仅仅基于值来推断类型,它还会考虑值出现的上下文。一般情况下,这可以正常工作,但有时可能会出现意外情况。了解在类型推断中如何使用上下文将有助于你识别并解决这些意外情况。
在 JavaScript 中可以在不改变代码行为的情况下对表达式进行拆分,换句话说,以下的内联形式和引用形式是等价的:
function setLanguage(language) {
console.log(language);
}
// 内联形式
setLanguage('JavaScript');
// 引用形式
let language = 'JavaScript';
setLanguage(language);
将以上代码重构成 TypeScript 的话,也是可以正常运行:
function setLanguage(language: string) {
console.log(language);
}
setLanguage('JavaScript'); // OK
let language = 'JavaScript';
setLanguage(language); // OK
在 TypeScript 项目中,相比字符串来说,一种更好的方式是使用字符串字面量联合类型,比如这样:
type Language = 'JavaScript' | 'TypeScript' | 'Python';
function setLanguage(language: Language) {
console.log(language);
}
setLanguage('JavaScript'); // OK,内联形式
let language = 'JavaScript';
setLanguage(language); // Error,引用形式
在上述代码中,对于第二个 setLanguage()
方法调用,TypeScript 编译器会提示以下错误信息:
Argument of type 'string' is not assignable to parameter
of type 'Language'.(2345)
什么地方出错了呢?对于内联形式,TypeScript 从函数声明中知道参数的类型为 Language
类型。字符串字面量 'JavaScript'
可以赋值给该类型,所以可以正常编译。
而当你定义一个变量时,TypeScript 必须在赋值时推断出其类型,即 language 变量的类型是 string
类型。很明显 string
类型与 Language
类型是不匹配的,所以编译器会提示以上的错误信息。
对于上述问题,有两种比较好的解决方案。第一种是使用类型声明限制变量的类型:
let language: Language = 'JavaScript';
setLanguage(language); // OK
这种方案也有另一种好处,即可以检查拼写错误,比如把 'JavaScript'
写成 'Javascript'
。而另一种方案是使用 const
关键字:
const language = 'JavaScript';
setLanguage(language); // OK
通过使用 const 关键字,我们告诉类型检查器,language
变量不能更改。因此,此时 TypeScript 可以为 language
变量推断出更精确的类型,即 'JavaScript'
字符串字面量类型,所以该方案能通过类型检查器检查。
这里的根本问题是,我们已经将值与使用它的上下文分开了。有时这是可以的,但通常是不行的。接下来我们将介绍几种情况,其中上下文的丢失可能会导致错误,并向你展示如何修复这些问题。
二、元组类型
除了字符串字面量类型之外,元组类型也会出现问题。假设你正在使用一个地图可视化工具,该工具允许你以编程的方式平移地图:
// 参数是(经度,纬度)对
function panTo(location: [number, number]) {
console.log("latitude: " + location[0]);
console.log("longitude: " + location[1]);
}
panTo([10, 20]); // OK,(A)
const loc = [10, 20];
panTo(loc); // Error,(B)
在以上代码的中,对于第 (B) 行,TypeScript 编译器会抛出以下错误信息:
Argument of type 'number[]' is not assignable to parameter
of type '[number, number]'.
与前面的示例一样,你已经将值与其上下文分离出来。对于第 (A) 行中的参数 [10, 20]
可以直接赋值给 [number, number]
元组类型。而对于第 (B) 行,TypeScript 推断出 loc
的类型是 number[]
,该类型无法正常赋值给元组类型。
那么,如何在不求助于 any
类型的情况下修复这个错误呢?其实你已经将 loc
声明为 const
,但这并不会有任何作用。这时你仍然可以提供一个类型声明,让 TypeScript 了解你的意图:
const loc: [number, number] = [10, 20];
panTo(loc); // OK
另一种解决方案是提供一个 “const context”:
function panTo(location: [number, number]) {
console.log("latitude: " + location[0]);
console.log("longitude: " + location[1]);
}
const loc = [10, 20] as const; // (A)
panTo(loc); // Error
在第 (A) 行中,我们使用了 TypeScript 3.4 版本引入了 const 断言,对 loc 变量使用 const 断言之后,loc 变量的类型将被推断为 readonly [10, 20]
类型,而不是 number[]
类型。
Argument of type 'readonly [10, 20]' is not assignable to parameter of type '[number, number]'.
The type 'readonly [10, 20]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.(2345)
出现上述错误的原因也很明显,因为 panTo
函数的类型签名并没有说明不能修改 location
参数的内容,而对于 loc 参数却含有一个 readonly 类型,所以这种类型是不匹配的。针对这个问题,最好的解决方式是为 panTo 函数添加一个 readonly 注解:
function panTo(location: readonly [number, number]) {
console.log("latitude: " + location[0]);
console.log("longitude: " + location[1]);
}
const loc = [10, 20] as const; // (A)
panTo(loc);
“const context” 可以很好地解决在推断中丢失上下文的问题,但是它有一个缺点:如果你在定义中犯了一个错误(假设你向元组添加了第三个元素),那么错误将在函数调用点而不是在定义处标记。这可能令人困惑,特别是当错误发生在一个深度嵌套的对象中:
function panTo(location: readonly [number, number]) {
console.log("latitude: " + location[0]);
console.log("longitude: " + location[1]);
}
const loc = [10, 20, 30] as const; // 错误发生点
panTo(loc); // Error
对于以上代码,TypeScript 编译器会提示以下错误信息:
Argument of type 'readonly [10, 20, 30]' is not assignable to parameter of type 'readonly [number, number]'.
Types of property 'length' are incompatible.
Type '3' is not assignable to type '2'.(2345)
三、对象
当你从包含一些字符串文本或元组的对象中提取常量时,也会出现将值与其上下文分离的问题。例如:
type Language = "JavaScript" | "TypeScript" | "Python";
interface GovernedLanguage {
language: Language;
organization: string;
}
function complain(language: GovernedLanguage) {
/* ... */
}
complain({ language: "TypeScript", organization: "Microsoft" }); // OK
const ts = {
language: "TypeScript",
organization: "Microsoft",
};
complain(ts); // Error
对于以上代码,TypeScript 编译器会提示以下错误信息:
Argument of type '{ language: string; organization: string; }' is not assignable to parameter of type 'GovernedLanguage'.
Types of property 'language' are incompatible.
Type 'string' is not assignable to type 'Language'.(2345)
出现以上错误的原因是因为 ts 对象中 language
属性的类型被推断为 string
类型,该类型与 GovernedLanguage
类型不匹配。为了解决这个问题,我们可以参考前面的解决方案:
3.1 方案一
const ts: GovernedLanguage = {
language: "TypeScript",
organization: "Microsoft",
};
complain(ts); // OK
3.2 方案二
const ts = {
language: "TypeScript",
organization: "Microsoft",
} as const;
complain(ts); // OK
四、回调函数
当你将回调函数传递给另一个函数时,TypeScript 会使用上下文来推断该回调函数的参数类型:
function callWithRandomNumbers(fn:
(n1: number, n2: number) => void) {
fn(Math.random(), Math.random());
}
callWithRandomNumbers((a, b) => {
a; // Type is number
b; // Type is number
console.log(a + b);
});
由于 callWithRandomNumbers 的类型声明,a 和 b 的类型被推断为 number 类型。如果将回调函数改为函数表达式,则会丢失该上下文并得到 implicitly has an 'any' type.
的错误:
function callWithRandomNumbers(fn:
(n1: number, n2: number) => void) {
fn(Math.random(), Math.random());
}
const fn = (a, b) => {
// Parameter 'a' implicitly has an 'any' type.(7006)
// Parameter 'b' implicitly has an 'any' type.(7006)
console.log(a + b);
}
callWithRandomNumbers(fn);
要解决这个问题,也很简单只要为 fn 函数的参数添加类型注解:
const fn = (a: number, b: number) => {
console.log(a + b);
}
callWithRandomNumbers(fn);
或者为整个函数表达式添加一个类型声明:
const fn: (n1: number, n2: number) => void = (a, b) => {
console.log(a + b);
}
callWithRandomNumbers(fn);
五、参考资源
- effective-typescript-specific-ways-improve
一文读懂 TypeScript 泛型及应用
了不起的 TypeScript 入门教程了不起的 TypeScript 入门教程
你不知道的 Blob你不知道的 Blob
聚焦全栈,专注分享 Angular、TypeScript、Node.js 、Spring 技术栈等全栈干货。
回复 0 进入重学TypeScript学习群
回复 1 获取全栈修仙之路博客地址