每日前端夜话0x38
每日前端夜话,陪你聊前端。
每天晚上18:00准时推送。
正文共:3749 字
预计阅读时间: 12 分钟
翻译:疯狂的技术宅 原文:http://2ality.com/2018/04/type-notation-typescript.html
阅读本文后,你应该能够理解以下代码的含义:
1interface Array<T> {
2 concat(...items: Array<T[] | T>): T[];
3 reduce<U>(
4 callback: (state: U, element: T, index: number, array: T[]) => U,
5 firstState?: U): U;
6 ···
7}
如果你认为这段代码非常神秘 —— 那么我同意你的意见。但是(我希望证明)这些符号还是相对容易学习的。一旦你能理解它们,就能马上全面、精确的理解这种代码,从而无需再去阅读冗长的英文说明。
TypeScript 有一个在线运行环境【http://www.typescriptlang.org/play/】。为了得到最全面的信息,你应该在 “Options” 菜单中打开所有选项开关。这相当于在 --strict
模式下运行TypeScript编译器。
我在用 TypeScript 时总是喜欢打开 --strict
开关设置。没有它,程序可能会稍微好写一点,但是你也失去了静态类型检查的好处。目前此设置能够开启以下子设置:
--noImplicitAny
:如果 TypeScript 无法推断类型,则必须指定它。这主要用于函数和方法的参数:使用此设置,你必须对它们进行注释。--noImplicitThis
:如果 this
的类型不清楚则会给出提示信息。--alwaysStrict
:尽可能使用 JavaScript 的严格模式。--strictNullChecks
:null
不属于任何类型(除了它自己的类型,null
),如果它是可接受的值,则必须明确指定。--strictFunctionTypes
:对函数类型更加严格的检查。--strictPropertyInitialization
:如果属性的值不能是 undefined
,那么它必须在构造函数中进行初始化。更多信息:TypeScript 手册中的“编译器选项”【https://www.typescriptlang.org/docs/handbook/compiler-options.html】一章。
在本文中,我们把类型看作是一组值的集合。 JavaScript 语言(不是TypeScript!)有7种类型:
undefined
的集合。false
和 true
的集合。所有这些类型都是 dynamic:可以用在运行时。
TypeScript 为 JavaScript 带来了额外的层:静态类型。这些仅在编译或类型检查源代码时存在。每个存储位置(变量或属性)都有一个静态类型,用于预测其动态值。类型检查可确保这些预测能够实现。还有很多可以进行 静态 检查(不运行代码)的东西。例如,如果函数 f(x)
的参数 x
是静态类型 number
,则函数调用 f('abc')
是非法的,因为参数 'abc'
是错误的静态类型。
变量名后的冒号开始 类型注释:冒号后的类型签名用来描述变量可以接受的值。例如以代码告诉 TypeScript 变量 “x” 只能存储数字:
1let x: number;
你可能想知道用 undefined
去初始化 x
是不是违反了静态类型。 TypeScript 不会允许这种情况出现,因为在为它赋值之前不允许操作 x
。
即使在 TypeScript 中每个存储位置都有静态类型,你也不必总是明确的去指定它。 TypeScript 通常可以对它的类型进行推断。例如如果你写下这行代码:
1let x = 123;
然后 TypeScript 会推断出 x
的静态类型是 number
。
在类型注释的冒号后面出现的是所谓的类型表达式。这些范围从简单到复杂,并按如下方式创建。
基本类型是有效的类型表达式:
请注意,“undefined
作为值“ 和 ”undefined
作为类型” 都写做 undefined
。根据你使用它的位置,被解释为值或类型。 null
也是如此。
你可以通过类型运算符对基本类型进行组合的方式来创建更多的类型表达式,这有点像使用运算符 union(∪
)和intersection(∩
)去合并集合。
下面介绍 TypeScript 提供的一些类型运算符。
数组在 JavaScript 中扮演以下两个角色(有时是两者的混合):
数组 arr
被用作列表有两种方法表示 ,其元素都是数字:
1let arr: number[] = [];
2let arr: Array<number> = [];
通常如果存在赋值的话,TypeScript 就可以推断变量的类型。在这种情况下,实际上你必须帮它解决类型问题,因为在使用空数组时,它无法确定元素的类型。
稍后我们将回到尖括号表示法(Array<number>
)。
如果你想在数组中存储二维坐标点,那么就可以把这个数组当作元组去用。看上去是这个样子:
1let point: [number, number] = [7, 5];
在这种情况下,你不需要类型注释。
另外一个例子是 Object.entries(obj)
的返回值:一个带有一个 [key,value] 对的数组,它用于描述 obj
的每个属性。
1> Object.entries({a:1, b:2})
2[ [ 'a', 1 ], [ 'b', 2 ] ]
Object.entries()
的返回值类型是:
1Array<[string, any]>
以下是函数类型的例子:
1(num: number) => string
这个类型是一个函数,它接受一个数字类型参数并且返回值为字符串。在类型注释中使用这种类型(String
在这里是个函数)的例子:
1const func: (num: number) => string = String;
同样,我们一般不会在这里使用类型注释,因为 TypeScript 知道 String
的类型,因此可以推断出 func
的类型。
以下代码是一个更实际的例子:
1function stringify123(callback: (num: number) => string) {
2 return callback(123);
3}
由于我们使用了函数类型来描述 stringify123()
的参数 callback
,所以TypeScript 拒绝以下函数调用。
1f(Number);
但它接受以下函数调用:
1f(String);
对函数的所有参数进行注释是一个很好的做法。你还可以指定返回值类型(不过 TypeScript 非常擅长去推断它):
1function stringify123(callback: (num: number) => string): string {
2 const num = 123;
3 return callback(num);
4}
void
是函数的特殊返回值类型:它告诉 TypeScript 函数总是返回 undefined
(显式或隐式):
1function f1(): void { return undefined } // OK
2function f2(): void { } // OK
3function f3(): void { return 'abc' } // error
标识符后面的问号表示该参数是可选的。例如:
1function stringify123(callback?: (num: number) => string) {
2 const num = 123;
3 if (callback) {
4 return callback(num); // (A)
5 }
6 return String(num);
7}
在 --strict
模式下运行 TypeScript 时,如果事先检查时发现 callback
没有被省略,它只允许你在 A 行进行函数调用。
TypeScript支持 ES6 参数默认值【http://exploringjs.com/es6/ch_parameter-handling.html#sec_parameter-default-values】:
1function createPoint(x=0, y=0) {
2 return [x, y];
3}
默认值可以使参数可选。通常可以省略类型注释,因为 TypeScript 可以推断类型。例如它可以推断出 x
和 y
都是 number
类型。
如果要添加类型注释,应该这样写:
1function createPoint(x:number = 0, y:number = 0) {
2 return [x, y];
3}
你还可以用 ES6 rest operator 【http://exploringjs.com/es6/ch_parameter-handling.html#sec_rest-parameters】进行 TypeScript 参数定义。相应参数的类型必须是数组:
1function joinNumbers(...nums: number[]): string {
2 return nums.join('-');
3}
4joinNumbers(1, 2, 3); // '1-2-3'
在JavaScript中,有时候变量会是有几种类型之中的一种。要描述这些变量,可以使用 union types。例如,在下面的代码中,x
是 null
类型或 number
类型:
1let x = null;
2x = 123;
x
的类型可以描述为 null | number
:
1let x: null|number = null;
2x = 123;
类型表达式 s | t
的结果是类型 s
和 t
在集合理论意义上的联合(正如我们之前看到的那样,两个集合)。
下面让我们重写函数 stringify123()
:这次我们不希望参数 callback
是可选的。应该总是调用它。如果调用者不想传入一个函数,则必须显式传递 null
。实现如下。
1function stringify123(
2 callback: null | ((num: number) => string)) {
3 const num = 123;
4 if (callback) { // (A)
5 return callback(123); // (B)
6 }
7 return String(num);
8}
请注意,在行 B 进行函数调用之前,我们必须再次检查 callback
是否真的是一个函数(行A)。如果没有检查,TypeScript 将会报告错误。
类型为 T
的可选参数和类型为 undefined|T
的参数非常相似。 (另外对于可选属性也是如此。)
主要区别在于你可以省略可选参数:
1function f1(x?: number) { }
2f1(); // OK
3f1(undefined); // OK
4f1(123); // OK
But you can’t omit parameters of type
但是你不能省略 undefined|T
类型的参数:
1function f2(x: undefined | number) { }
2f2(); // error
3f2(undefined); // OK
4f2(123); // OK
在许多编程语言中,null
是所有类型的一部分。例如只要 Java 中的参数类型为 String
,就可以传递 null
而Java 不会报错。
相反,在TypeScript中,undefined
和 null
由单独的不相交类型处理。如果你想使它们生效,必须要有一个类型联合,如 undefined|string
和 null|string
。
与Arrays类似,对象在 JavaScript 中扮演两个角色(偶尔混合和/或更加动态):
我们将在本文章中忽略 object-as-dictionaries。顺便说一句,无论如何,map 通常是比字典的更好选择。
接口描述 objects-as-records 。例如:
1interface Point {
2 x: number;
3 y: number;
4}
TypeScript 类型系统的一大优势在于它的结构上,而不是在命名上。也就是说,接口 Point
能够匹配适当结构的所有对象:
1function pointToString(p: Point) {
2 return `(${p.x}, ${p.y})`;
3}
4pointToString({x: 5, y: 7}); // '(5, 7)'
相比之下,Java 的标称类型系统需要类来实现接口。
如果可以省略属性,则在其名称后面加上一个问号:
1interface Person {
2 name: string;
3 company?: string;
4}
接口内还可以包含方法:
1interface Point {
2 x: number;
3 y: number;
4 distance(other: Point): number;
5}
使用静态类型,可以有两个级别:
同理:
普通变量通过 const
,let
等引入。类型变量通过尖括号( <>
)引入。例如以下代码包含类型变量 T
,通过 <T>
引入。
1interface Stack<T> {
2 push(x: T): void;
3 pop(): T;
4}
你可以看到类型参数 T
在 Stack
的主体内出现两次。因此,该接口可以直观地理解如下:
Stack
是一堆值,它们都具有给定的类型 T
。每当你提到 Stack
时,必须写 T
。接下来我们会看到究竟该怎么用。.push()
接受类型为 T
的值。.pop()
返回类型为 T
的值。如果使用 Stack
,则必须为 T
指定一个类型。以下代码显示了一个虚拟栈,其唯一目的是匹配接口。
1const dummyStack: Stack<number> = {
2 push(x: number) {},
3 pop() { return 123 },
4};
map 在 TypeScript 中的定义。例如:
1const myMap: Map<boolean,string> = new Map([
2 [false, 'no'],
3 [true, 'yes'],
4]);
函数(和方法)也可以引入类型变量:
1function id<T>(x: T): T {
2 return x;
3}
你可以按以下方式使用此功能。
1id<number>(123);
由于类型推断,还可以省略类型参数:
1id(123);
函数可以将其她的类型参数传给接口、类等:
1function fillArray<T>(len: number, elem: T) {
2 return new Array<T>(len).fill(elem);
3}
类型变量 T
在这段代码中出现三次:
fillArray
:引入类型变量elem:T
:使用类型变量,从参数中选择它。Array
:将 T
传递给 Array
的构造函数。这意味着:我们不必显式指定Array<T>
的类型 T
—— 它是从参数 elem
中推断出来的:
1const arr = fillArray(3, '*');
2 // Inferred type: string[]
让我们用前面学到的知识来理解最开始看到的那段代码:
1interface Array<T> {
2 concat(...items: Array<T[] | T>): T[];
3 reduce<U>(
4 callback: (state: U, element: T, index: number, array: T[]) => U,
5 firstState?: U): U;
6 ···
7}
这是一个Array的接口,其元素类型为 T
,每当使用这个接口时必须填写它:
.concat()
有零个或多个参数(通过 rest 运算符定义)。其中每一个参数中都具有类型 T[]|T
。也就是说,它是一个 T
类型的数组或是一个 T
值。.reduce()
引入了自己的类型变量 U
。 U
表示以下实体都具有相同的类型(你不需要指定,它是自动推断的):state
of callback()
(which is a function)state
是 callback()
的参数(这是一个函数)callback()
callback()
的返回.reduce()
的可选参数 firstState
.reduce()
.reduce()
的返回callback
还将获得一个 element
参数,其类型与 Array 元素具有相同的类型 T
,参数 index
是一个数字,参数 array
是 T
的值。