TypeScript学习文档——高级篇
- TypeScript学习高级篇第一章:变量声明
- 1.1 var变量声明
- 1.2 作用域法则
- 1.3 变量捕获的怪癖
- 1.4 let变量声明
- 1.5 块级作用域
- 1.6 重复声明和投影
- 1.7 块级作用域变量捕获
- 1.8
const声明 - 1.9
let与const比较 - 1.10 解构
- 1.11 数组析构
- 1.12 元组解构
- 1.13 对象解构
- 1.14 Function声明
- 1.15 展开
- TypeScript学习高级篇第二章:类型推断
- 2.1 最佳公共类型
- 2.2 上下文类型
- TypeScript学习高级篇第三章:枚举
- 3.1 数值型枚举
- 3.2 字符串枚举
- 3.3 异构枚举
- 3.4 计算型和常量型成员
- 3.5 联合枚举和枚举成员类型
- 3.6 运行时的枚举
- 3.7 编译时的枚举
- 3.8 环境枚举
- 3.9 对象与枚举
- TypeScript学习高级篇第四章:公共类型
- 4.1
Partial - 4.2
Required - 4.3
Readonly - 4.4
Record - 4.5
Pick - 4.6
Omit - 4.7
Exclude - 4.8
Extract - 4.9
NonNullable - 4.10
Parameters - 4.11
ConstructorParameters - 4.12
ReturnType - 4.13 InstanceType
- 4.14 ThisParameterType
- 4.15 OmitThisParameter
- 4.16 ThisType
- 4.17 字符串操作类型
- 4.1
- TypeScript学习高级篇第五章:Symbols
- 5.1
unique symbol - 5.2 知名的 Symbols
- 5.2.1 Symbol.hasInstance
- 5.2.2 Symbol.isConcatSpreadable
- 5.2.3 Symbol.iterator
- 5.2.4 Symbol.match
- 5.2.5 Symbol.replace
- 5.2.6 Symbol.search
- 5.2.7 Symbol.species
- 5.2.8 Symbol.split
- 5.2.9 Symbol.toPrimitive
- 5.2.10 Symbol.toStringTag
- 5.2.11 Symbol.unscopables
- 5.1
- TypeScript学习高级篇第六章:类型兼容性
- 6.1 关于健全性的说明
- 6.2 起步
- 6.3 对比两个函数
- 6.4 函数参数的双差性
- 6.5 可选参数和其他参数
- 6.6 带有重载的函数
- 6.7 枚举
- 6.8 类
- 6.9 类中的私有和受保护成员
- 6.10 泛型
- 6.11 子类型与赋值
- 6.12
any,unknown,object,void,undefined,null, 和never可分配性
- TypeScript学习高级篇第七章:迭代器和生成
- 7.1 遍历
- 7.1.1
Iterable接口 - 7.1.2
for ... of声明 - 7.1.3
for ... of与for ... in声明
- 7.1.1
- 7.2 代码生成
- 7.2.1 生成目标 ES5 和 ES3
- 7.2.2 ECMAScript 2015(ES6) 和 更高版本
- TypeScript学习高级篇第八章:装饰器(Decorators)
- 8.1 简介
- 8.2 装饰器
- 8.3 装饰器工厂
- 8.4 装饰器构成
- 8.5 装饰器评估
- 8.6 类装饰器
- 7.1 遍历
TypeScript学习高级篇第一章:变量声明
let和const是JavaScript中变量声明的两个相对较新的概念。正如我们前面提到的, let 在某些方面与 var 相似,但允许用户避免在JavaScript中遇到的一些常见的 "麻烦"。
const是let的一个扩展,它可以防止重新赋值给一个变量。
由于TypeScript是JavaScript的扩展,该语言自然支持 let 和 const 。在这里,我们将进一步阐述这些新的声明,以及为什么它们比 var 更适合。
如果你已经不经意地使用了JavaScript,那么下一节可能是刷新你记忆的一个好方法。如果你对JavaScript中 var 声明的所有怪癖非常熟悉,你可能会发现跳过前面会更容易。
1.1 var变量声明
在JS中声明一个变量,传统上都是用var关键字来完成。
var a = 10
正如你可能已经发现的,我们刚刚声明了一个名为a的变量,其值为10 。
我们也可以在一个函数中声明一个变量:
function f() {
var message = "Hello, world!";
return message;
}
而我们也可以在其他函数中访问这些相同的变量:
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
};
}
var g = f();
g(); // returns '11'
在上面这个例子中, g 捕获了 f 中声明的变量 a 。在 g 被调用的任何时候, a 的值都将与 f 中 a 的值相联系。
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
f(); // returns '2'
1.2 作用域法则
对于那些习惯于其他语言的人来说, var 声明有一些奇怪的作用域范围规则。以下面的例子为例:
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true); // 返回 '10'
f(false); // 返回 'undefined'
有些读者可能会对这个例子产生怀疑。变量 x 是在 if 块中声明的,但我们却能从该块之外访问它。这是因为 var 声明可以在其包含的函数、模块、命名空间或全局范围内的任何地方访问(所有这些我们将在后面讨论),而不考虑包含的块。有些人把这称为 var 作用域或函数作用域。参数也是函数作用域。
这些作用域规则会导致几种类型的错误。它们加剧的一个问题是,多次声明同一个变量并不是一个错误。
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
也许对于一些有经验的JavaScript开发者来说,这很容易被发现,但是内部 for-loop 会意外地覆盖变量 i ,因为 i 指的是同一个函数范围的变量。正如有经验的开发者现在所知道的,类似的各种bug会在代码审查中溜走,并会成为无尽的挫折来源。
1.3 变量捕获的怪癖
花点时间猜一猜下面这段话的输出是什么:
for (var i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100 * i);
}
对于那些不熟悉的人来说, setTimeout 将尝试在一定数量的毫秒后执行一个函数(尽管要等待其他东西停止运行)。最后的结果是十行10。
许多JavaScript开发人员对这种行为非常熟悉,但如果你感到惊讶,你肯定不是一个人。大多数人都希望输出的结果是:1 2 3 4 5 6 7 8 9 10。
还记得我们前面提到的关于变量捕获的问题吗?我们传递给 setTimeout 的每个函数表达式实际上都是指同一范围内的同一个 i 。
让我们花点时间考虑一下这意味着什么。setTimeout 将在若干毫秒之后运行一个函数,但只有在 for循环停止执行之后;当 for 循环停止执行时, i 的值是 10 。因此,每次给定的函数被调用时,它将打 印出 10 !
一个常见的解决方法是使用IIFE--一个立即调用的函数表达式--来捕获每次迭代的 i 。
for (var i = 0; i < 10; i++) {
// 通过调用一个带有其当前值的函数
// 捕捉'i'的当前状态
(function (i) {
setTimeout(function () {
console.log(i);
}, 100 * i);
})(i);
}
这种看起来很奇怪的模式其实是很常见的。参数列表中的 i 实际上是对 for 循环中声明的 i 的影子,但由于我们对它们的命名相同,所以我们不必对循环体进行过多的修改。
1.4 let变量声明
现在你已经发现 var 有一些问题,这正是 let 语句被引入的原因。除了使用的关键字外, let 语句的写法与 var 语句相同。
let hello = 'hello'
关键的区别不在语法上,而在语义上,我们现在要深入研究
1.5 块级作用域
当一个变量使用 let 声明时,它使用了一些人所说的词法范围或块法范围。与用 var 声明的变量不同, block-scope块级作用域变量的作用域会泄露给其包含的函数, 而在其最近的包含块或 for-loop之外是不可见的。
function f(input: boolean) {
let a = 100;
if (input) {
// 引用'a'仍然可以
let b = a + 1;
return b;
}
// 错误:这里不存在'b'。
return b;
}
在这里,我们有两个局部变量 a 和 b 。a 的作用域仅限于 f 的主体,而 b 的作用域仅限于包含 if 语句的块。
在 catch 子句中声明的变量也有类似的作用域规则
try {
throw "oh no!";
} catch (e) {
console.log("Oh well.");
}
// Error: 这里不存在'e'。
console.log(e);
块级作用域变量的另一个属性是,在它们被实际声明之前,它们不能被读或写到。虽然这些变量在它们的整个作用域中都是 "存在 "的,但是直到它们被声明之前的所有点都是它们的时间死角的一部分。这只是一种复杂的说法,你不能在 let 语句之前访问它们,幸运的是TypeScript会让你知道这一点。
a++; // 在声明之前使用'a'是非法的。
let a;
需要注意的是,你仍然可以在声明之前捕获一个块范围的变量。唯一的问题是,在声明之前调用该函数是非法的。如果以ES2015为目标,现代运行时将抛出一个错误;然而,现在TypeScript是允许的,不会将此作为一个错误报告。
function foo() {
// 可以捕捉到 "a"。
return a;
}
// 在声明'a'之前非法调用'foo'。
// runtimes应该在这里抛出一个错误
foo();
let a;
1.6 重复声明和投影
对于var声明,我们提到,你声明了多少次变量并不重要,你只是得到了一个。
function f(x) {
var x; var x;
if (true) {
var x;
}
}
在上面的例子中,所有关于 x 的声明实际上指的是同一个 x ,这是完全有效的。这往往会成为错误的根源。值得庆幸的是, let 的声明并不那么宽容。
let x = 10;
let x = 20; // 错误:不能在同一范围内重新声明'x'。
变量不一定要都是块范围的,TypeScript才会告诉我们有一个问题。
function f(x) {
let x = 100; // 错误:干扰了参数声明
}
function g() {
let x = 100;
var x = 100; // 错误:不能同时有'x'的声明
}
这并不是说一个块作用域变量永远不能和一个函数作用域变量一起声明。区块作用域变量只是需要在一个明显不同的区块中声明。
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // 返回 0
f(true, 0); // 返回 100
在一个更加嵌套的作用域中引入一个新名字的行为被称为投影。这是一把双刃剑,因为它可以在意外影射的情况下自行引入某些错误,同时也可以防止某些错误。例如,想象一下我们之前用 let 变量编写的sumMatrix函数:
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
这个版本的循环实际上会正确地执行求和,因为内循环的 i 会对外循环的 i 产生阴影。
为了写出更清晰的代码,通常应避免使用投影。虽然在某些情况下,利用它可能是合适的,但你应该使用你的最佳判断。
1.7 块级作用域变量捕获
当我们第一次触及用 var 声明捕获变量的想法时,我们简要地讨论了变量一旦被捕获是如何行动的。为了给大家一个更好的直观印象,每次运行一个作用域时,它都会创建一个变量的 "环境"。这个环境和它捕获的变量甚至在它的作用域内的所有东西都执行完毕后仍然存在。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function () {
return city;
};
}
return getCity();
}
因为我们已经从它的环境中捕获了 city ,所以尽管 if 块已经执行完毕,我们仍然能够访问它。
回想一下,在我们之前的 setTimeout 例子中,我们最终需要使用IIFE来捕获 for 循环的每个迭代中的变量状态。实际上,我们所做的是为我们捕获的变量创建一个新的变量环境。这有点麻烦,但幸运的是,在TypeScript中你再也不用这么做了。
当声明为循环的一部分时, let 声明的行为有很大的不同。这些声明并不只是给循环本身引入一个新的环境,而是在每个迭代中创建一个新的范围。因为这就是我们在IIFE中所做的事情,我们可以改变我们以前的 setTimeout 的例子,只使用 let 声明。
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i);
}, 100 * i);
}
和预期一样会打印:0 1 2 3 4 5 6 7 8 9
1.8 const声明
const声明是声明变量的另一种方式
const numLivesForCat = 9;
它们就像 let 声明一样,但正如它们的名字所暗示的,一旦它们被绑定,它们的值就不能被改变。换句话说,它们有和 let 一样的范围规则,但你不能重新赋值给它们。
这不应该与它们所指的值是不可改变的想法相混淆。
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
};
// 错误
kitty = {
name: "Danielle",
numLives: numLivesForCat,
};
// 以下都正确
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
除非你采取特定的措施来避免它,否则常量变量的内部状态仍然是可以修改的。幸运的是,TypeScript允许你指定一个对象的成员是 readonly 的。
1.9 let与const比较
鉴于我们有两种具有类似范围语义的声明,我们很自然地会问自己应该使用哪一种。像大多数广泛的问题一样,答案是:这取决于。
根据最小特权原则,除了那些你打算修改的声明外,所有的声明都应该使用 const。其理由是,如果一个变量不需要被写入,那么在同一个代码库中工作的其他人就不应该自动能够写入该对象,他们需要考虑是否真的需要重新赋值给该变量。在推理数据流时,使用 const 也会使代码更可预测。
使用你的最佳判断,如果适用的话,请与你的团队其他成员协商此事。
下面文档大部分内容都使用 let 声明。
1.10 解构
解构赋值语法是一种 Javascript表达式。通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。
1.11 数组析构
最简单的解构形式是数组解构赋值。
let input = [1, 2];
let [first, second] = input;
console.log(first); // 输出 1
console.log(second); // 输出 2
这将创建两个新的变量,命名为 first 和 second 。这等同于使用索引,但要方便得多。
first = input[0];
second = input[1];
解构也适用于已经声明的变量。
// 交换变量
[first, second] = [second, first];
而且是带参数的函数:
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f([1, 2]);
你可以使用语法 ... 为列表中的剩余项目创建一个变量
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 输出 1
console.log(rest); // 输出 [ 2, 3, 4 ]
当然,由于这是JavaScript,你可以直接忽略你不关心的拖尾元素:
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1
1.12 元组解构
元组可以像数组一样被去结构化;去结构化的变量得到相应元组元素的类型:
let tuple: [number, string, boolean] = [7, "hello", true];
let [a, b, c] = tuple; // a: number, b: string, c: boolean
对一个元组进行解构,超出其元素的范围是一个错误:
let [a, b, c, d] = tuple; // 错误,索引3处没有元素
和数组一样,你可以用 ... 对元组的其余部分进行解构,以得到一个更短的元组:
let [a, ...bc] = tuple; // bc: [string, boolean]
let [a, b, c, ...d] = tuple; // d: [], 空 tuple
或者忽略尾部元素,或者忽略其他元素:
let [a] = tuple; // a: number
let [, b] = tuple; // b: string
1.13 对象解构
你也可以做对象的解构:
let o = { a: "foo", b: 12, c: "bar",};
let { a, b } = o;
这就从 o.a 和 o.b 中创建了新的变量 a 和 b 。注意,如果你不需要 c ,你可以跳过它。 就像数组去结构化一样,你可以不用声明就进行赋值:
({ a, b } = { a: "baz", b: 101 });
请注意,我们必须用圆括号包围这个语句。JavaScript通常将{作为块的开始来解析。
你可以使用语法 ... 为对象中的剩余项目创建一个变量:
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
- 属性重命名
你也可以给属性起不同的名字:
let { a: newName1, b: newName2 } = o;
这里的语法开始变得混乱了。你可以把 a: newName1 读作 "a as newName1" 。方向是从左到右,就像你写的一样:
let newName1 = o.a;
let newName2 = o.b;
令人困惑的是,这里的冒号并不表示类型。如果你指定了类型,仍然需要写在整个结构解构之后。
let { a, b }: { a: string; b: number } = o;
- 默认值
默认值让你指定一个默认值,以防一个属性未被定义:
function keepWholeObject(wholeObject: { a: string; b?: number }) {
let { a, b = 1001 } = wholeObject;
}
在这个例子中, b? 表示 b 是可选的,所以它可能是未定义的。 keepWholeObject 现在有一个 wholeObject 的变量,以及属性 a 和 b ,即使 b 是未定义的。
1.14 Function声明
去结构化在函数声明中也起作用。对于简单的情况,这是很直接的。
type C = { a: string; b?: number };
function f({ a, b }: C): void {
// ...
}
但是对于参数来说,指定默认值是比较常见的,而用解构的方式来获得默认值是很棘手的。首先,你需要记住把模式放在默认值之前。
function f({ a = "", b = 0 } = {}): void {
// ...
}
f();
然后,你需要记住在 destructured属性上给可选属性一个默认值,而不是主初始化器。记住, C的定义是b可选的。
function f({ a, b = 0 } = { a: "" }): void {
// ...
}
f({ a: "yes" }); // 正确,b = 0
f(); // 正确, 默认 { a: "" }, 然后默认为 b = 0
f({}); // 错误,如果你提供一个参数,'a'是必须的
小心使用解构。正如前面的例子所展示的,除了最简单的析构表达式之外,任何东西都会令人困惑。这在深度嵌套的结构化中尤其如此,即使不堆积重命名、默认值和类型注释,也会变得非常难以理解。尽量保持结构化表达式的小而简单。你总是可以自己写出解构会产生的赋值。
1.15 展开
展开操作符与解构相反。它允许你将一个数组分散到另一个数组中,或者将一个对象分散到另一个对象中。比如说:
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
这使 bothPlus 的值为 [0, 1, 2, 3, 4, 5] 。展开创建first和second的浅层拷贝。它们不会因 为展开而改变。
你也可以展开对象。
let defaults = {
food: "spicy",
price: "$$",
ambiance: "noisy"
};
let search = {
...defaults,
food: "rich"
};
现在的 search 是 { food: "rich", price: "$$", ambiance: "noisy" } 。对象展开比数组展开更复杂。像数组展开一样,它从左到右进行,但结果仍然是一个对象。这意味着展开对象中较晚出现的属性会覆盖较早出现的属性。因此,如果我们修改前面的例子,在最后展开:
let defaults = {
food: "spicy",
price: "$$",
ambiance: "noisy"
};
let search = {
food: "rich",
...defaults
};
然后, defaults 中的食物属性覆盖了 food: "rich" ,这不是我们在这种情况下想要的。
对象传播也有其他一些令人惊讶的限制。首先,它只包括一个对象自己的、可列举的属性。基本上,这意味着当你传播一个对象的实例时,你会失去方法。
class C {
p = 12;
m() {}
}
let c = new C();
let clone = {
...c
};
clone.p; // 正确
clone.m(); // 错误!
TypeScript编译器不允许从通用函数中展开类型参数。该功能预计将在未来的语言版本中出现。
TypeScript学习高级篇第二章:类型推断
在TS中,有几个地方在没有显式类型注释的情况下,使用类型推理来提供类型信息。例如,在这段代码中:
//let x: number
let x = 3
x 变量的类型被推断为 number 。这种推断发生在初始化变量和成员、设置参数默认值和确定函数返回类型时。
在大多数情况下,类型推断是直截了当的。在下面的章节中,我们将探讨类型推断的一些细微差别。
2.1 最佳公共类型
当从几个表达式中进行类型推断时,这些表达式的类型被用来计算一个 "最佳公共类型"。比如说:
// let x: (number | null)[]
let x = [0, 1, null];
为了推断上面例子中 x 的类型,我们必须考虑每个数组元素的类型。这里我们得到了两个数组类型的选择: number 和 null 。最佳公共类型算法考虑了每个候选类型,并选择了与所有其他候选类型兼容的类型。
因为最佳公共类型必须从所提供的候选类型中选择,所以在某些情况下,类型有共同的结构,但没有一个类型是所有候选类型的超级类型。比如说:
// let zoo: (Rhino | Elephant | Snake)[]
let zoo = [new Rhino(), new Elephant(), new Snake()];
理想情况下,我们可能希望 zoo 被推断为 Animal[] ,但是因为数组中没有严格意义上的 Animal 类型的对象,所以我们没有对数组元素类型进行推断。为了纠正这一点,当没有一个类型是所有其他候选类型的超级类型时,就明确地提供类型。
// let zoo: Animal[]t
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
当没有找到最好的共同类型时,产生的推论是联合数组类型, (Rhino | Elephant | Snake)[] 。
2.2 上下文类型
在TS的某些情况下,类型推理也在"另一个方向"发挥作用。这被称为”上下文类型化“。当表达式的类型被他的位置所暗示时,上下文类型就发生了。例如:
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.button);
console.log(mouseEvent.kangaroo); // ? 在'MouseEvent'类型上不存在'kangaroo'属性。
};
在这里,TypeScript 类型检查器使用 window.onmousedown 函数的类型来推断赋值右侧的函数表达式的类型。当它这样做时,它能够推断出 mouseEvent 参数的类型,它确实包含一个按钮button属性,但不包含袋鼠kangaroo属性。
这样做的原因是 window 已经在其类型中声明了 onmousedown 。
// 声明有一个名为'window'的全局变量
declare var window: Window & typeof globalThis;
// 这被声明为(简化版)。
interface Window extends GlobalEventHandlers {
// ...
}
// 其中定义了很多已知的处理程序事件
interface GlobalEventHandlers {
onmousedown: ((this: GlobalEventHandlers, ev: MouseEvent) => any) | null;
// ...
}
TypeScript足够聪明,在其他情况下也能推断出类型:
window.onscroll = function (uiEvent) {
// ? 属性 "button" 不存在于 "Event"类型上。
console.log(uiEvent.button);
};
基于上述函数被分配给 Window.onscroll 的事实,TypeScript知道 uiEvent 是一个 UIEvent ,而不是像前面的例子那样是 MouseEvent 。 UIEvent 对象不包含按钮属性,所以TypeScript会抛出一个错误。
如果这个函数不在上下文类型的位置,这个函数的参数将隐含有类型 any ,并且不会发出错误(除非你使用 noImplicitAny选项)。
const handler = function (uiEvent) {
console.log(uiEvent.button); // <- 正确
};
我们也可以明确地给函数的参数提供类型信息,以覆盖任何上下文的类型。
window.onscroll = function (uiEvent: any) {
console.log(uiEvent.button); // <- 现在也没有错误
};
然而,这段代码将记录 undefined 的内容,因为 uiEvent 没有名为按钮的属性。
上下文类型化在很多情况下都适用。常见的情况包括函数调用的参数、赋值的右侧、类型断言、对象和数组字面量的成员,以及返回语句。上下文类型也作为最佳普通类型的候选类型。比如说:
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}
在这个例子中,最佳普通类型有一组四个候选者。 Animal , Rhino , Elephant 和 Snake 。其中,Animal 可以被最佳共同类型算法所选择。
TypeScript学习高级篇第三章:枚举
Enums 是TypeScript的少数功能之一,它不是JavaScript的类型级扩展。
枚举允许开发者定义一组命名的常量。使用枚举可以使其更容易记录意图,或创建一组不同的情况。TypeScript提供了基于数字和字符串的枚举。
3.1 数值型枚举
我们首先从数字枚举开始,如果你来自自其他语言,可能会更熟悉它。一个枚举可以用 enum关键字来定义。
enum Direction {
Up = 1,
Down,
Left,
Right,
}
上面,我们有一个数字枚举,其中 Up 被初始化为 1 ,所有下面的成员从这一点开始自动递增。换句话说, Direction.Up的值是 1 , Down 是 2 , Left 是 3 , Right 是 4 。
如果我们愿意,我们可以完全不使用初始化器:
enum Direction {
Up,
Down,
Left,
Right,
}
这里,Up的值是0,Down是1,依次类推。这种自动递增的行为对于我们可能不关心成员值本身,但关心每个值与同一枚举中的其他值不同的情况很有用。
使用枚举很简单:只需将任何成员作为枚举本身的一个属性来访问,并使用枚举的名称来声明类型:
enum UserResponse {
No = 0,
Yes = 1,
}
function respond(recipient: string, message: UserResponse): void {
// ...
}
respond("Princess Caroline", UserResponse.Yes);
数字枚举可以混合在计算和常量成员中(见下文)。简而言之,没有初始化器的枚举要么需要放在第一位,要么必须放在用数字常量或其他常量枚举成员初始化的数字枚举之后。换句话说,下面的情况是不允许的:
enum E {
A = getSomeValue(),
B,
// ? Enum成员必须有初始化器。
}
3.2 字符串枚举
字符串枚举是一个类似的概念,但有一些细微的运行时差异,如下文所述。在一个字符串枚举中,每个成员都必须用一个字符串字头或另一个字符串枚举成员进行常量初始化。
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
虽然字符串枚举没有自动递增的行为,但字符串枚举有一个好处,那就是它们可以很好地 "序列化"。换句话说,如果你在调试时不得不读取一个数字枚举的运行时值,这个值往往是不透明的--它本身并不传达任何有用的意义(反向映射往往可以),字符串枚举允许你在代码运行时给出一个有意义的、可读的值,与枚举成员本身的名称无关。
3.3 异构枚举
从技术上讲,枚举可以与字符串和数字成员混合,但不清楚为什么你会想这样做:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
除非你真的想以一种巧妙的方式利用JavaScript的运行时行为,否则建议你不要这样做。
3.4 计算型和常量型成员
每个枚举成员都有一个与之相关的值,可以是常量,也可以是计算值。一个枚举成员被认为是常数,如果:
- 它是枚举中的第一个成员,它没有初始化器,在这种情况下,它被赋值为
0:
// E.X is constant:
enum E { X,}
- 它没有一个初始化器,而且前面的枚举成员是一个数字常数。在这种情况下,当前枚举成员的值将是前一个枚举成员的值加 1 :
// 'E1'和'E2'中的所有枚举成员都是常数。
enum E1 { X, Y, Z,}
enum E2 { A = 1, B, C,}
枚举成员用一个常量枚举表达式进行初始化。常量枚举表达式是TypeScript表达式的一个子集,可以在编译时进行完全评估。一个表达式是一个常量枚举表达式,如果它是:
- 枚举表达式的字面意思(基本上是一个字符串字面量或一个数字字面量);
- 对先前定义的常量枚举成员的引用(可以来自不同的枚举);
- 一个括号内的常量枚举表达式;
- 应用于常量枚举表达式的
+,-,~单项运算符之一 ; +,-,*,/,%,<<,>>,&,|,^以常量枚举表达式为操作数的二元运算符。
如果常量枚举表达式被评估为 NaN 或 Infinity ,这是一个编译时错误。
在所有其他情况下,枚举成员被认为是计算出来的。
enum FileAccess {
// 常量成员
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 计算成员
G = "123".length,
}
3.5 联合枚举和枚举成员类型
有一个特殊的常量枚举成员的子集没有被计算:字面枚举成员。字面枚举成员是一个没有初始化值的常量枚举成员,或者其值被初始化为:
- 任何字符串(例如:
"foo","bar","baz") - 任何数字字头(例如: 1 , 100)
- 应用于任何数字字面的单数减号(例如: -1 , -100 )
当一个枚举中的所有成员都有枚举的字面价值时,一些特殊的语义就会发挥作用。
首先,枚举成员也成为了类型。例如,我们可以说某些成员只能有一个枚举成员的值:
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// ? 类型 'ShapeKind.Square' 不能被分配给类型 'ShapeKind.Circle'
radius: 100,
}
另一个变化是枚举类型本身有效地成为每个枚举成员的联盟。通过联合枚举,类型系统能够利用这一事实,即它知道存在于枚举本身的精确的值集。正因为如此,TypeScript可以捕捉到我们可能错误地比较数值的错误。比如说:
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
// ? 这个条件将总是返回'true',因为'E.Foo'和'E.Bar'的类型没有重合。
//...
}
}
在这个例子中,我们首先检查了 x 是否不是 E.Foo 。如果这个检查成功了,那么我们的 || 就会短 路, if 语句的主体就会运行。然而,如果检查没有成功,那么 x 就只能是 E.Foo ,所以看它是否等于 E.Bar 就没有意义了。
3.6 运行时的枚举
枚举是在运行时存在的真实对象。例如,下面这个枚举
enum E {
X,
Y,
Z,
}
实际上可以被传递给函数:
enum E {
X,
Y,
Z,
}
function f(obj: { X: number }) {
return obj.X;
}
// 可以正常工作,因为'E'有一个名为'X'的属性,是一个数字。
f(E);
3.7 编译时的枚举
尽管Enum是在运行时存在的真实对象, keyof 关键字的工作方式与你对对象的预期不同。相反,使用keyof typeof来获得一个将所有Enum键表示为字符串的类型。
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
/**
* 这相当于:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
function printImportant(key: LogLevelStrings, message: string) {
const num = LogLevel[key];
if (num <= LogLevel.WARN) {
console.log("Log level key is:", key);
console.log("Log level value is:", num);
console.log("Log level message is:", message);
}
}
printImportant("ERROR", "This is a message");
- 反向映射
除了为成员创建一个带有属性名称的对象外,数字枚举的成员还可以得到从枚举值到枚举名称的反向映射。例如,在这个例子中:
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
TypeScript将其编译为以下的JavaScript:
"use strict";
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
在这段生成的代码中,一个枚举被编译成一个对象,它同时存储了正向 ( name -> value )和反向 ( value -> name ) 的映射关系。对其他枚举成员的引用总是以属性访问的方式发出,而且从不内联。
请记住,字符串枚举成员根本不会被生成反向映射。
const枚举
在大多数情况下,枚举是一个完全有效的解决方案。然而有时要求比较严格。为了避免在访问枚举值时支付额外的生成代码和额外的间接性的代价,可以使用 const 枚举。常量枚举是使用我们枚举上的 const 修饰符来定义的。
const enum Enum {
A = 1,
B = A * 2,
}
常量枚举只能使用常量枚举表达式,与普通枚举不同,它们在编译过程中被完全删除。常量枚举成员在使用地点被内联。这是可能的,因为常量枚举不能有计算的成员。
const enum Direction {
Up,
Down,
Left,
Right,
}
let directions = [
Direction.Up,
Direction.Down,
Direction.Left,
Direction.Right,
];
在生成的代码中,将变成:
"use strict";
let directions = [
0 /* Up */ ,
1 /* Down */ ,
2 /* Left */ ,
3 /* Right */ ,
];
3.8 环境枚举
环境枚举是用来描述已经存在的枚举类型的形状。
declare enum Enum {
A = 1,
B,
C = 2,
}
环境枚举和非环境枚举之间的一个重要区别是,在常规枚举中,如果其前面的枚举成员被认为是常量,那么没有初始化器的成员将被认为是常量。相反,一个没有初始化器的环境(和非常量)枚举成员总是被认为是计算的。
3.9 对象与枚举
在现代TypeScript中,你可能不需要一个枚举,因为一个对象的常量就足够了:
const enum EDirection {
Up,
Down,
Left,
Right,
}
const ODirection = {
Up: 0,
Down: 1,
Left: 2,
Right: 3,
} as const;
// (enum member) EDirection.Up = 0
EDirection.Up;
// (property) Up: 0
ODirection.Up;
// 将枚举作为一个参数
function walk(dir: EDirection) {}
// 它需要一个额外的行来拉出数值
type Direction = typeof ODirection[keyof typeof ODirection];
function run(dir: Direction) {}
walk(EDirection.Left);
run(ODirection.Right);
与TypeScript的枚举相比,支持这种格式的最大理由是,它使你的代码库与JavaScript的状态保持一致, when/if 枚举被添加到JavaScript中,那么你可以转移到额外的语法。
TypeScript学习高级篇第四章:公共类型
TypeScript 提供了几个实用类型,以促进常见的类型转换。这些实用程序在全局范围内可用。“
4.1 Partial
构建一个类型,将 Type 的所有属性设置为可选。这个工具将返回一个表示给定类型的所有子集的类型。
例子:
interface Todo {
title: string;
description: string;
}
function updateTodo(todo: Todo, fieldsToUpdate: Partial) {
return { ...todo, ...fieldsToUpdate };
}
const todo1:Todo = {
title: "organize desk",
description: "clear clutter",
};
//因为可以将所有属性设置为可选,可选如果不设置即为undefined,所以拿此来测试
const todo2:Partial = updateTodo(todo1, {
description: undefined,
})
4.2 Required
构建一个由 Type 的所有属性组成的类型,设置为必填。与 Partial 相反:
interface Props {
a?: number;
b?: string;
}
const obj: Props = { a: 5 };
// error,类型 "{ a: number; }" 中缺少属性 "b",但类型 "Required" 中需要该属性
const obj2: Required = { a: 5 };
4.3 Readonly
构建一个类型, Type 的所有属性设置为 readonly ,这意味着构建的类型的属性不能被重新设置值。
interface Todo {
title: string;
}
const todo: Readonly = {
title: "Delete inactive users",
};
// error
todo.title = "Hello";
这个工具对于表示将在运行时失败的赋值表达式很有用(即当试图重新分配一个冻结对象的属性时)。
function freeze(obj: Type): Readonly;
4.4 Record
构建一个对象类型,其属性键是 Keys ,其属性值是 Type 。这个工具可以用来将一个类型的属性映射到另一个类型:
interface CatInfo {
age: number;
breed: string;
}
type CatName = "miffy" | "boris" | "mordred";
const cats: Record = {
miffy: { age: 10, breed: "Persian" },
boris: { age: 5, breed: "Maine Coon" },
mordred: { age: 16, breed: "British Shorthair" },
};
// const cats: Record
console.log(cats.boris) // { age: 5, breed: 'Maine Coon' }
4.5 Pick
通过从 Type 中选取属性集合 Keys (属性名或属性名的联合)来构造一个类型:
interface Todo {
title: string;
description: string;
completed: boolean;
}
type TodoPreview = Pick;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
};
// const todo: TodoPreview
todo;
4.6 Omit
通过从 Type 中选取所有属性,然后删除Keys(属性名或属性名的联合)来构造一个类型。
interface Todo {
title: string;
description: string;
completed: boolean;
createdAt: number;
}
type TodoPreview = Omit;
const todo: TodoPreview = {
title: "Clean room",
completed: false,
createdAt: 1615544252770,
};
// const todo: TodoPreview
todo;
type TodoInfo = Omit;
const todoInfo: TodoInfo = {
title: "Pick up kids",
description: "Kindergarten closes at 5pm",
};
// const todoInfo: TodoInfo
todoInfo;
4.7 Exclude
通过从 Type 中排除所有可分配给 ExcludedUnion 的联盟成员来构造一个类型。
// type T0 = "b" | "c"
type T0 = Exclude<"a" | "b" | "c", "a">;
// type T1 = "c"
type T1 = Exclude<"a" | "b" | "c", "a" | "b">;
// type T2 = string | number
type T2 = Exclude void), Function>;
4.8 Extract
通过从 Type 中提取可分配给 Union 的所有 union 成员,构造一个类型。
// type T0 = "a"
type T0 = Extract<"a" | "b" | "c", "a" | "f">
// type T1 = () => void
type T1 = Extract void), Function>
4.9 NonNullable
通过从 Type 中排除 null 和 undefined 来构造一个类型。
// type T0 = string | number
type T0 = NonNullable;
// type T1 = string[]
type T1 = NonNullable;
4.10 Parameters
从一个函数类型 Type 的参数中使用的类型构建一个元组类型。
declare function f1(arg: { a: number; b: string }): void;
// type T0 = []
type T0 = Parameters<() => string>;
// type T1 = [s: string]
type T1 = Parameters<(s: string) => void>;
// type T2 = [arg: unknown]
type T2 = Parameters<(arg: T) => T>;
/*
type T3 = [arg: {
a: number;
b: string;
}]
*/
type T3 = Parameters;
// type T4 = unknown[]
type T4 = Parameters;
// type T5 = never
type T5 = Parameters;
// type T6 = never
type T6 = Parameters;
// type T7 = never
type T7 = Parameters;
4.11 ConstructorParameters
从构造函数的类型中构造一个元组或数组类型。它产生一个具有所有参数类型的元组类型(如果 Type 不是一个函数,则为 never 类型)。
// type T0 = [message?: string]
type T0 = ConstructorParameters;
// type T1 = string[]
type T1 = ConstructorParameters;
// type T2 = [pattern: string | RegExp, flags?: string]
type T2 = ConstructorParameters;
// type T3 = unknown[]
type T3 = ConstructorParameters;
// type T4 = never
type T4 = ConstructorParameters;
4.12 ReturnType
构建一个由函数 Type 的返回类型组成的类型。如果是泛型则是unknown。
declare function f1(): { a: number; b: string };
// type T0 = string
type T0 = ReturnType<() => string>;
// type T1 = void
type T1 = ReturnType<(s: string) => void>;
// type T2 = unknown
type T2 = ReturnType<() => T>;
// type T3 = number[]
type T3 = ReturnType<() => T>;
/*
type T4 = {
a: number;
b: string;
}
*/
type T4 = ReturnType;
// type T5 = any
type T5 = ReturnType;
// type T6 = never
type T6 = ReturnType;
// type T7 = any 报错
type T7 = ReturnType;
// type T8 = any 报错
type T8 = ReturnType
4.13 InstanceType
构建一个由 Type 中构造函数的实例类型组成的类型。
class C {
x = 0;
y = 0;
}
// type T0 = C
type T0 = InstanceType;
// type T1 = any
type T1 = InstanceType;
// type T2 = never
type T2 = InstanceType;
// type T3 = any
type T3 = InstanceType;
// type T4 = any
type T4 = InstanceType;
4.14 ThisParameterType
提取一个函数类型的 this 参数的类型,如果该函数类型没有 this 参数,则为 unknown 。
function toHex(this: Number) {
return this.toString(16);
}
// n: number
function numberToString(n: ThisParameterType) {
return toHex.apply(n);
}
4.15 OmitThisParameter
移除 Type 的 this 参数。如果 Type 没有明确声明的 this 参数,结果只是 Type 。否则,一个没有 this 参数的新函数类型将从 Type 创建。泛型被擦除,只有最后的重载签名被传播到新的函数类型。
function toHex(this: Number) {
return this.toString(16);
}
const fiveToHex: OmitThisParameter = toHex.bind(5);
console.log(fiveToHex());
4.16 ThisType
这个工具并不返回一个转换后的类型。相反,它作为一个上下文的 this 类型的标记。注意,必须启用noImplicitThis 标志才能使用这个工具。
ts类型中的&表示交叉类型, 主要用于组合现有的对象类型。
type ObjectDescriptor = {
data?: D;
methods?: M & ThisType; // 方法中的 'this' 类型是 D & M
};
function makeObject(desc: ObjectDescriptor): D & M {
let data: object = desc.data || {};
let methods: object = desc.methods || {};
return { ...data, ...methods } as D & M;
}
let obj = makeObject({
data: { x: 0, y: 0 },
methods: {
moveBy(dx: number, dy: number) {
this.x += dx;
this.y += dy;
},
},
});
obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);
在上面的例子中,makeObject的参数中的 methods 对象有一个包括 ThisType 的上下文类型,因此方法对象中 this 的类型是 { x: number, y: number } & { moveBy(dx: number, dy: number): number } 。注意 methods 属性的类型如何同时是推理目标和方法中 this 类型的来源。 ThisType 标记接口只是在 lib.d.ts 中声明的一个空接口。除了在对象字面的上下文类型中被识别之外,该接口的行为与任何空接口一样。
4.17 字符串操作类型
Uppercase
Lowercase
Capitalize
Uncapitalize
TypeScript包括一组类型,可以在类型系统中用于字符串操作。你可以在 Template Literal Types 文档 中找到这些工具的用法。
TypeScript学习高级篇第五章:Symbols
从ECMAScript 2015(ES6)开始, symbol 是一种原始的数据类型,就像 number 和 string 一样。
symbol值是通过调用Symbol构造函数创建的。
let sym1 = Symbol();
let sym2 = Symbol("key"); // 可选的字符串 key
Symbols 是不可改变的,而且是独一无二的。
let sym2 = Symbol("key");
let sym3 = Symbol("key");
sym2 === sym3; // false, symbols 是唯一的
就像字符串一样,Symbols可以被用作对象属性的键。
const sym = Symbol();
let obj = {
[sym]: "value",
};
console.log(obj[sym]); // "value"
Symbols也可以与计算属性声明结合起来,以声明对象属性和类成员。
const getClassNameSymbol = Symbol();
class C {
[getClassNameSymbol]() {
return "C";
}
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"
5.1 unique symbol
为了能够将 symbols 作为唯一的字面符号,提供了一个特殊的类型 unique symbol 。 unique symbol 是 symbol 的一个子类型,只在调用 Symbol() 或 Symbol.for() 或明确的类型注释时产生。这种类型只允许在常量声明和只读静态属性中使用,为了引用一个特定的唯一符号,你必须使用typeof操作符。每个对唯一符号的引用都意味着一个完全独特的身份,它与一个给定的声明相联系。
declare const sym1: unique symbol;
// sym2只能是一个常数参考。
let sym2: unique symbol = Symbol();
// ? 类型为 "唯一符号 "的变量必须是 "const"类型。
// 运行正确--指的是一个独特的 symbol,但其身份与'sym1'相联系。
let sym3: typeof sym1 = sym1;
// 也是正确的
class C {
static readonly StaticSymbol: unique symbol = Symbol();
}
因为每个 unique symbol 都有一个完全独立的身份,没有两个 unique symbol 类型是可以相互分配或比较的。
const sym2 = Symbol();
const sym3 = Symbol();
// 这个条件将总是返回'false',因为'typeof sym2'和'typeof sym3'的类型没有重合。
if (sym2 === sym3) {
// ...
}
5.2 知名的 Symbols
除了用户定义的 symbols 外,还有著名的内置 symbols。内置符号被用来表示内部语言行为。
下面是一个著名的 symbols 列表:
5.2.1 Symbol.hasInstance
一个确定构造函数对象,是否识别一个对象为构造函数的实例之一的方法。由instanceof操作符的语义调用。
5.2.2 Symbol.isConcatSpreadable
一个布尔值,表示一个对象应该被Array.prototype.concat平铺到其数组元素。
5.2.3 Symbol.iterator
返回一个对象的默认迭代器的方法。被 for-of 语句的语义所调用。
5.2.4 Symbol.match
一个正则表达式方法,与字符串的正则表达式相匹配。由 String.prototype.match 方法调用。
5.2.5 Symbol.replace
一个正则表达式方法,用于替换一个字符串中匹配的子串。由 String.prototype.replace 方法调用。
5.2.6 Symbol.search
一个正则表达式方法,返回字符串中符合正则表达式的索引。由 String.prototype.search 方法调用。
5.2.7 Symbol.species
一个函数值的属性,是用于创建派生对象的构造函数。
5.2.8 Symbol.split
一个正则表达式方法,在符合正则表达式的索引处分割一个字符串。由 String.prototype.split 方法调用。
5.2.9 Symbol.toPrimitive
将一个对象转换为一个相应的基元值的方法。由ToPrimitive抽象操作调用。
5.2.10 Symbol.toStringTag
一个字符串值,用于创建一个对象的默认字符串描述。由内置方法 Object.prototype.toString 调用。
5.2.11 Symbol.unscopables
一个对象,其自身的属性名是被排除在相关对象的 'with' 环境绑定之外的属性名。
TypeScript学习高级篇第六章:类型兼容性
TypeScript中的类型兼容性是基于结构子类型的。结构分型是一种完全基于其成员的类型关系的方式。
这与名义类型不同。考虑一下下面的代码:
interface Pet {
name: string;
}
class Dog {
name: string;
}
let pet: Pet;
// 正确,因为结构化类型
pet = new Dog();
在像 C# 或 Java 这样的名义类型语言中,相应的代码将是一个错误,因为 Dog 类没有明确地描述自己是 Pet 接口的实现者。
TypeScript的结构类型系统是根据JavaScript代码的典型写法设计的。因为JavaScript广泛使用匿名对象,如函数表达式和对象字面量,用结构类型系统而不是命名类型系统来表示JavaScript库中的各种关系要自然得多。
6.1 关于健全性的说明
TypeScript 的类型系统允许某些在编译时无法知道的操作是安全的。当一个类型系统具有这种属性时, 它被称为不 "健全"。我们仔细考虑了 TypeScript 允许不健全行为的地方,在这篇文档中,我们将解释这 些发生的地方以及它们背后的动机情景。
6.2 起步
TypeScript的结构类型系统的基本规则是,如果 y 至少有与 x 相同的成员,那么 x 与 y 是兼容的。wwww
interface Pet {
name: string;
}
let pet: Pet;
// dog's 推断类型是 { name: string; owner: string; }
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
pet = dog
为了检查 dog 是否可以被分配给 pet ,编译器检查 pet 的每个属性,以找到 dog 中相应的兼容属 性。在这种情况下, dog 必须有一个名为 name 的成员,它是一个字符串。它有,所以赋值是允许的。
在检查函数调用参数时,也使用了同样的赋值规则。
interface Pet {
name: string;
}
let dog = { name: "Lassie", owner: "Rudd Weatherwax" };
function greet(pet: Pet) {
console.log("Hello, " + pet.name);
}
greet(dog); // 正确
请注意, dog 有一个额外的 owner 属性,但这并不产生错误。在检查兼容性时,只考虑目标类型(本例中为 Pet)的成员。
这个比较过程是递归进行的,探索每个成员和子成员的类型。
6.3 对比两个函数
虽然比较原始类型和对象类型是相对直接的,但什么样的函数应该被认为是兼容的,这个问题就有点复杂了。让我们从两个函数的基本例子开始,这两个函数只在参数列表上有所不同:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // 正确
x = y; // 错误
为了检查 x 是否可以分配给 y ,我们首先看一下参数列表。 x 中的每个参数在 y 中都必须有一个类型兼容的对应参数。注意,参数的名称不被考虑,只考虑它们的类型。在这种情况下, x 中的每个参数在y中都有一个对应的兼容参数,所以这个赋值是允许的。
第二个赋值是一个错误,因为 y 有一个 x 没有的必要的第二个参数,所以这个赋值是不允许的。
你可能想知道为什么我们允许像例子中的 y = x 那样 "丢弃 "参数。这个赋值被允许的原因是,忽略额外的函数参数在JavaScript中其实很常见。例如, Array#forEach 为回调函数提供了三个参数:数组元素、其索引和包含数组。尽管如此,提供一个只使用第一个参数的回调是非常有用的:
let items = [1, 2, 3];
// 不要强迫这些额外参数
items.forEach((item, index, array) => console.log(item));
// 应该没有问题!
items.forEach((item) => console.log(item));
现在让我们看看如何处理返回类型,使用两个只因返回类型不同的函数:
let x = () => ({ name: "Alice" });
let y = () => ({ name: "Alice", location: "Seattle" });
x = y; // 正确
y = x; // 错误,因为x()缺少一个location属性
类型系统强制要求源函数的返回类型是目标类型的返回类型的一个子类型。
6.4 函数参数的双差性
enum EventType {
Mouse,
Keyboard,
}
interface Event {
timestamp: number;
}
interface MyMouseEvent extends Event {
x: number;
y: number;
}
interface MyKeyEvent extends Event {
keyCode: number;
}
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/* ... */
}
// 不健全,但有用且常见
listenEvent(EventType.Mouse, (e: MyMouseEvent) => console.log(e.x + "," + e.y));
// 在健全性存在的情况下,不可取的选择
listenEvent(EventType.Mouse, (e: Event) =>
console.log((e as MyMouseEvent).x + "," + (e as MyMouseEvent).y)
);
listenEvent(EventType.Mouse, ((e: MyMouseEvent) =>
console.log(e.x + "," + e.y)) as (e: Event) => void);
// 仍然不允许(明确的错误)。对于完全不兼容的类型强制执行类型安全
listenEvent(EventType.Mouse, (e: number) => console.log(e));
当这种情况发生时,你可以让TypeScript通过编译器标志 strictFunctionTypes 引发错误。
6.5 可选参数和其他参数
在比较函数的兼容性时,可选参数和必需参数是可以互换的。源类型的额外可选参数不是错误,而目标类型的可选参数在源类型中没有对应的参数也不是错误。
当一个函数有一个剩余参数时,它被当作是一个无限的可选参数系列。
从类型系统的角度来看,这是不健全的,但从运行时的角度来看,可选参数的概念一般不会得到很好的加强,因为在这个位置传递 undefined 的参数对大多数函数来说是等价的。
激励性的例子是一个函数的常见模式,它接受一个回调,并用一些可预测的(对程序员)但未知的(对类型系统)参数数量来调用它。
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* ... 用'args'调用回调 ... */
}
// 不健全 - invokeLater "可能 "提供任何数量的参数
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
// 令人困惑的是(x和y实际上是需要的),而且是无法发现的
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));
6.6 带有重载的函数
当一个函数有重载时,源类型中的每个重载必须由目标类型上的兼容签名来匹配。这保证了目标函数可以在所有与源函数相同的情况下被调用。
6.7 枚举
枚举与数字兼容,而数字与枚举兼容。来自不同枚举类型的枚举值被认为是不兼容的。比如说:
enum Status {
Ready,
Waiting,
}
enum Color {
Red,
Blue,
Green,
}
let status = Status.Ready;
status = Color.Green; // 错误
6.8 类
类的工作方式与对象字面类型和接口类似,但有一个例外:它们同时具有静态和实例类型。当比较一个类类型的两个对象时,只有实例的成员被比较。静态成员和构造函数不影响兼容性。
class Animal {
feet: number;
constructor(name: string, numFeet: number) {}
}
class Size {
feet: number;
constructor(numFeet: number) {}
}
let a: Animal;
let s: Size;
a = s; // 正确
s = a; // 正确
6.9 类中的私有和受保护成员
一个类中的私有成员和保护成员会影响其兼容性。当一个类的实例被检查兼容性时,如果目标类型包含一个私有成员,那么源类型也必须包含一个源自同一类的私有成员。同样地,这也适用于有保护成员的实例。这允许一个类与它的超类进行赋值兼容,但不允许与来自不同继承层次的类进行赋值兼容,否则就会有相同的形状。
6.10 泛型
因为TypeScript是一个结构化的类型系统,类型参数只在作为成员类型的一部分被消耗时影响到结果类型。比如说:
interface Empty {}
let x: Empty;
let y: Empty;
x = y; // 正确,因为y符合x的结构
在上面, x 和 y 是兼容的,因为它们的结构没有以区分的方式使用类型参数。通过给 Empty 增加一个成员来改变这个例子,显示了这是如何工作的。
interface NotEmpty {
data: T;
}
let x: NotEmpty;
let y: NotEmpty;
x = y; // 错误,因为x和y不兼容
这样一来,一个指定了类型参数的泛型类型就像一个非泛型类型一样。
对于没有指定类型参数的泛型,兼容性的检查是通过指定any来代替所有未指定的类型参数。然后产生的类型被检查是否兼容,就像在非泛型的情况下一样。
比如说:
let identity = function (x: T): T {
// ...
};
let reverse = function (y: U): U {
// ...
};
identity = reverse; // 正确, 因为 (x: any) => any 匹配 (y: any) => any
6.11 子类型与赋值
到目前为止,我们已经使用了 "兼容",这并不是语言规范中定义的一个术语。在TypeScript中,有两种兼容性:子类型和赋值。这些不同之处只在于,赋值扩展了子类型的兼容性,允许赋值到 any ,以及赋值到具有相应数值的 enum 。
语言中不同的地方使用这两种兼容性机制中的一种,取决于情况。在实际应用中,类型兼容性是由赋值兼容性决定的,即使是在 implements 和 extends 子句中。
6.12 any ,unknown ,object ,void ,undefined ,null , 和 never可分配性
下表总结了一些抽象类型之间的可分配性。行表示每个类型可被分配到什么,列表示什么可被分配到它们。"?"表示只有在关闭 strictNullChecks 时才是兼容的组合
| any | unknown | object | void | undefined | null | never | |
|---|---|---|---|---|---|---|---|
| any | ? | ? | ? | ? | ? | ? | |
| unknown | ? | ? | ? | ? | ? | ? | |
| object | ? | ? | ? | ? | ? | ? | |
| void | ? | ? | ? | ? | ? | ? | |
| undefined | ? | ? | ? | ? | ? | ? | |
| null | ? | ? | ? | ? | ? | ? | |
| never | ? | ? | ? | ? | ? | ? |
- 所有的东西都是可以分配给自己的。
any和unknown在可分配的内容方面是相同的,不同的是unknown不能分配给任何东西,除了any。unknown和never就像是彼此的反义词。一切都可以分配给unknown,never就可以分配给一切。没有任何东西可以分配给never,unknown不能分配给任何东西(除了any)。void不能赋值给任何东西,以下是例外情况:any、unknown、never、undefined和null(如果strictNullChecks是关闭的,详见表)。- 当
strictNullChecks关闭时,null和undefined与never类似:可赋值给大多数类型,大多数类型不可赋值给它们。它们可以互相赋值。 - 当
strictNullChecks打开时,null和undefined的行为更像void:除了any、unknown、never和void之外,不能赋值给任何东西(undefined总是可以赋值给void)。
TypeScript学习高级篇第七章:迭代器和生成
7.1 遍历
如果一个对象有 Symbol.iterator 属性的实现,它就被认为是可迭代的。一些内置类型,如 Array 、 Map 、 Set 、 String 、 Int32Array 、 Uint32Array 等,已经实现了它们的 Symbol.iterator 属性。对象上的 Symbol.iterator 函数负责返回要迭代的值的列表。
7.1.1 Iterable接口
Iterable是一个我们可以使用的类型,如果我们想接收上面列出的可迭代的类型。下面是一个例子:
// 传入的参数必须是可迭代的类型
function toArray(xs: Iterable): X[] {
return [...xs]
}
7.1.2 for ... of声明
for... of 在一个可迭代对象上循环,调用对象上的 Symbol.iterator 属性。下面是一个关于数组的简单 for... of 循环。
let someArray = [1, "string", false];
for (let entry of someArray) {
console.log(entry); // 1, "string", false
}
7.1.3 for ... of与for ... in 声明
for...of 和 for...in 语句都是在列表上进行迭代;但迭代的值是不同的, for...in 返回被迭代对象的键值列表,而 for...of 返回被迭代对象的数字属性值列表。 这里有一个例子可以证明这种区别:
let list = [4, 5, 6];
for (let i in list) {
console.log(i); // "0", "1", "2",
}
for (let i of list) {
console.log(i); // 4, 5, 6
}
另一个区别是 for...in 对任何对象进行操作;它作为一种检查该对象上的属性的方法。另一方面,for...of 主要对可迭代对象的值感兴趣。像 Map 和 Set 这样的内置对象实现了 Symbol.iterator 属性,允许访问存储的值。
// Set中的Iterable
let pets = new Set(["Cat", "Dog", "Hamster"]);
for (let pet in pets) {
console.log(pet); // 什么也不输出
}
for (let pet of pets) {
console.log(pet); // "Cat", "Dog", "Hamster"
}
// Map中的Iterable
let nums = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
])
for (let num in nums) {
console.log(num) // 什么也不输出
}
for (let num of nums) {
console.log(num) //[ 1, 'one' ] [ 2, 'two' ] [ 3, 'three' ]
}
7.2 代码生成
7.2.1 生成目标 ES5 和 ES3
当针对ES5或ES3兼容的引擎时,迭代器只允许在 Array 类型的值上使用。
在非数组值上使用 for...of 循环是一个错误,即使这些非数组值实现了 Symbol.iterator 属性。
例如,编译器将为 for... 的循环生成一个简单的 for 循环。
let numbers = [1, 2, 3];
for (let num of numbers) {
console.log(num);
}
将被生成为:
var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
var num = numbers[_i];
console.log(num);
}
7.2.2 ECMAScript 2015(ES6) 和 更高版本
当针对ECMAScipt 2015兼容的引擎时,编译器将生成 for...of 循环,以针对引擎中的内置迭代器实 现。
TypeScript学习高级篇第八章:装饰器(Decorators)
8.1 简介
随着TypeScript和ES6中类的引入,现在存在某些场景需要额外的功能,来支持注释或修改类和类成员。 装饰器提供了一种为类声明和成员添加注释和元编程语法的方法。装饰器是JavaScript的第二阶段建议,并作为TypeScript的一个实验性功能提供。
注意:装饰器是一个实验性的功能,在未来的版本中可能会改变。
要启用对装饰器的实验性支持,你必须在命令行或在 tsconfig.json 中启用experimentalDecorators 编译器选项。
- 命令行开启
tsc --target ES5 --experimentalDecorators
- tsconfig.json
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
8.2 装饰器
装饰器是一种特殊的声明,可以附加到类声明、方法、访问器、属性或参数上。装饰器使用 @expression 的形式,其中 expression 必须评估为一个函数,该函数将在运行时被调用,并带有关于被装饰的声明的信息。
例如,对于装饰器 @sealed ,我们可以将 sealed 的函数写成如下:
function sealed(target) {
// 对 "target"做一些事情 ...
}
8.3 装饰器工厂
如果我们想自定义装饰器如何应用于声明,我们可以写一个装饰器工厂。装饰器工厂是一个简单的函数,它返回将在运行时被装饰器调用的表达式。
我们可以用以下方式写一个装饰器工厂:
function color(value: string) {
// 这是装饰器工厂,它设置了
// 返回的装饰器函数
return function (target) {
// 这就是装饰器
// 用 "target" 和 "value"做一些事情...
};
}
8.4 装饰器构成
多个装饰器可以应用于一个声明,例如在一行中:
@f @g x
多行的语法:
@f
@g
x
当多个装饰器适用于一个声明时,它们的评估类似于数学中的函数组合。在这种模式下,当组合函数f和g时,所产生的组合 (f(g))(x) 等同于 f(g(x)) 。
因此,在TypeScript中对一个声明的多个装饰器进行评估时,会执行以下步骤:
- 每个装饰器的表达式都是自上而下地进行评估的。
- 然后将结果作为函数从下往上调用。
如果我们使用装饰器工厂,可以通过下面的例子观察这个评估顺序:
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
这将把这个输出打印到控制台:
first(): factory evaluated
second(): factory evaluated
second(): called
first(): called
8.5 装饰器评估
对于应用于类内各种声明的装饰器,有一个明确的顺序:
- 对于每个实例成员,首先是参数装饰器,然后是方法、访问器或属性装饰器。
- 对于每个静态成员,先是参数装饰器,然后是方法、存取器或属性装饰器。
- 参数装饰器被应用于构造函数。
- 类装饰器适用于类。
8.6 类装饰器
类装饰器就在类声明之前被声明。类装饰器被应用于类的构造函数,可以用来观察、修改或替换类定义。类装饰器不能在声明文件中使用,也不能在任何其他环境下使用(比如在 declare 类上)。
类装饰器的表达式在运行时将作为一个函数被调用,被装饰的类的构造器是它唯一的参数。
如果类装饰器返回一个值,它将用提供的构造函数替换类声明。
注意:如果你选择返回一个新的构造函数,必须注意维护原始原型。在运行时应用装饰器的逻辑不 会为你这样做。
下面是一个应用于 BugReport 类的类装饰器( @sealed )的例子。
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
我们可以用下面的函数声明来定义@sealed装饰器
// Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
当 @sealed 被执行时,它将同时封闭构造函数和它的原型,因此将阻止在运行时通过访问 BugReport.prototype 或通过定义 BugReport 本身的属性来向该类添加或删除任何进一步的功能(注意ES2015类实际上只是基于原型的构造函数的语法糖)。这个装饰器并不能阻止类对 BugReport 进行子类化。
接下来我们有一个如何覆盖构造函数以设置新的默认值的例子:
function reportableClassDecorator(constructor: T){
return class extends constructor {
reportingURL = "http://www...";
};
}
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
const bug = new BugReport("Needs dark mode");
console.log(bug.title); // 打印 "Needs dark mode"
console.log(bug.type); // 打印 "report"
// 注意,装饰器不会改变TypeScript的类型
// 因此,类型系统对新的属性`reportingURL`是不可知的。
bug.reportingURL;