十四、向类型添加特殊值
原文:
exploringjs.com/tackling-ts/ch_special-values.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
14.1?在带内添加特殊值
-
14.1.1?向类型添加
null 或undefined -
14.1.2?向类型添加符号
-
-
14.2?在带外添加特殊值
-
14.2.1?辨别式联合
-
14.2.2?其他类型的联合类型
-
理解类型的一种方式是将其视为值的集合。有时会有两个级别的值:
-
基本级别:普通值
-
元级别:特殊值
在本章中,我们将探讨如何向基本级别类型添加特殊值。
14.1?在带内添加特殊值
添加特殊值的一种方法是创建一个新类型,它是基本类型的超集,其中一些值是特殊的。这些特殊值被称为 哨兵。它们存在于 带内(想象在同一通道内),作为普通值的兄弟。
例如,考虑可读流的以下接口:
interface InputStream { getNextLine(): string; }
目前,
可能的方法包括:
-
在调用
.getNextLine() 之前需要调用一个额外的方法.isEof() 。 -
当到达 EOF 时,
.getNextLine() 会抛出异常。 -
EOF 的哨兵值。
接下来的两个小节描述了引入哨兵值的两种方法。
14.1.1?向类型添加 null 或 undefined
在使用严格的 TypeScript 时,没有简单的对象类型(通过接口、对象模式、类等定义)包括
type StreamValue = null | string; interface InputStream { getNextLine(): StreamValue; }
现在,每当我们使用
function countComments(is: InputStream) { let commentCount = 0; while (true) { const line = is.getNextLine(); // @ts-expect-error: Object is possibly 'null'.(2531) if (line.startsWith('#')) { // (A) commentCount++; } if (line === null) break; } return commentCount; }
在 A 行,我们不能使用字符串方法
function countComments(is: InputStream) { let commentCount = 0; while (true) { const line = is.getNextLine(); if (line === null) break; if (line.startsWith('#')) { // (A) commentCount++; } } return commentCount; }
现在,当执行到 A 行时,我们可以确信
14.1.2?向类型添加符号
我们还可以使用除
这是如何使用符号来表示 EOF 的方法:
const EOF = Symbol('EOF'); type StreamValue = typeof EOF | string;
为什么我们需要
14.2?在带外添加特殊值
如果一个方法可能返回 任何 值,我们该怎么办?如何确保基本值和元值不会混淆?这是可能发生的一个例子:
interface InputStream<T> { getNextValue(): T; }
无论我们选择什么值作为
解决方法是将普通值和特殊值分开,这样它们就不会混淆。特殊值单独存在被称为 带外(想象不同的通道)。
14.2.1?辨别式联合
辨别式联合 是几个对象类型的联合类型,它们都至少有一个共同的属性,即所谓的 辨别器。辨别器必须对每个对象类型具有不同的值 - 我们可以将其视为对象类型的 ID。
14.2.1.1?示例:InputStreamValue
在下面的例子中,
interface NormalValue<T> { type: 'normal'; // string literal type data: T; } interface Eof { type: 'eof'; // string literal type } type InputStreamValue<T> = Eof | NormalValue<T>; interface InputStream<T> { getNextValue(): InputStreamValue<T>; }
function countValues<T>(is: InputStream<T>, data: T) { let valueCount = 0; while (true) { // %inferred-type: Eof | NormalValue<T> const value = is.getNextValue(); // (A) if (value.type === 'eof') break; // %inferred-type: NormalValue<T> value; // (B) if (value.data === data) { // (C) valueCount++; } } return valueCount; }
最初,
14.2.1.2?示例:IteratorResult
在决定如何实现迭代器时,TC39 不想使用固定的哨兵值。否则,该值可能出现在可迭代对象中并破坏代码。一种解决方案是在开始迭代时选择一个哨兵值。相反,TC39 选择了一个带有共同属性
interface IteratorYieldResult<TYield> { done?: false; // boolean literal type value: TYield; } interface IteratorReturnResult<TReturn> { done: true; // boolean literal type value: TReturn; } type IteratorResult<T, TReturn = any> = | IteratorYieldResult<T> | IteratorReturnResult<TReturn>;
14.2.2?其他类型的联合类型
其他类型的联合类型可以像辨别联合一样方便,只要我们有手段来区分联合的成员类型。
一种可能性是通过唯一属性来区分成员类型:
interface A { one: number; two: number; } interface B { three: number; four: number; } type Union = A | B; function func(x: Union) { // @ts-expect-error: Property 'two' does not exist on type 'Union'. // Property 'two' does not exist on type 'B'.(2339) console.log(x.two); // error if ('one' in x) { // discriminating check console.log(x.two); // OK } }
另一种可能性是通过
type Union = [string] | number; function logHexValue(x: Union) { if (Array.isArray(x)) { // discriminating check console.log(x[0]); // OK } else { console.log(x.toString(16)); // OK } }
评论
第四部分:对象、类、数组和函数的类型
原文:
exploringjs.com/tackling-ts/pt_types-for-objects-classes-arrays-functions.html 译者:飞龙
协议:CC BY-NC-SA 4.0
下一步:15?对象的类型
十五、对象类型
原文:
exploringjs.com/tackling-ts/ch_typing-objects.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
15.1?对象扮演的角色
-
15.2?对象的类型
-
15.3?TypeScript 中的
Object vs.object -
15.3.1?纯 JavaScript:对象 vs.
Object 的实例 -
15.3.2?
Object (大写“O”)在 TypeScript 中:类Object 的实例 -
15.3.3?TypeScript 中的
object (小写“o”):非原始值 -
15.3.4?
Object vs.object :原始值 -
15.3.5?
Object vs.object :不兼容的属性类型
-
-
15.4?对象类型文字和接口
-
15.4.1?对象类型文字和接口之间的区别
-
15.4.2?接口在 TypeScript 中的结构工作
-
15.4.3?接口和对象类型文字的成员
-
15.4.4?方法签名
-
15.4.5?索引签名:对象作为字典
-
15.4.6?接口描述
Object 的实例 -
15.4.7?多余属性检查:何时允许额外属性?
-
-
15.5?类型推断
-
15.6?接口的其他特性
-
15.6.1?可选属性
-
15.6.2?只读属性
-
-
15.7?JavaScript 的原型链和 TypeScript 的类型
-
15.8?本章的来源
在本章中,我们将探讨在 TypeScript 中如何静态地为对象和属性进行类型化。
15.1?对象扮演的角色
在 JavaScript 中,对象可以扮演两种角色(总是至少其中一种,有时混合):
-
记录具有在开发时已知的固定数量的属性。每个属性可以具有不同的类型。
-
字典具有任意数量的属性,其名称在开发时未知。所有属性键(字符串和/或符号)具有相同的类型,属性值也是如此。
首先,我们将探索对象作为记录。我们将在本章的后面简要地遇到对象作为字典。
15.2?对象类型
对象有两种不同的一般类型:
-
Object 大写“O”是类Object 的所有实例的类型:let obj1: Object;
-
小写“o”的
object 是所有非原始值的类型:let obj2: object;
对象也可以通过它们的属性进行类型化:
// Object type literal let obj3: {prop: boolean}; // Interface interface ObjectType { prop: boolean; } let obj4: ObjectType;
在接下来的章节中,我们将更详细地研究所有这些对象类型的方式。
15.3?TypeScript 中的Object vs. object
15.3.1?纯 JavaScript:对象 vs. Object 的实例
在纯 JavaScript 中,有一个重要的区别。
一方面,大多数对象都是
> const obj1 = {}; > obj1 instanceof Object true
这意味着:
-
Object.prototype 在它们的原型链中:> Object.prototype.isPrototypeOf(obj1) true
-
它们继承了它的属性。
> obj1.toString === Object.prototype.toString true
另一方面,我们也可以创建没有
> const obj2 = Object.create(null); > Object.getPrototypeOf(obj2) null
> typeof obj2 'object' > obj2 instanceof Object false
15.3.2?TypeScript 中的Object (大写“O”):类Object 的实例
请记住,每个类
-
一个构造函数
C 。 -
描述构造函数实例的接口
C 。
同样,TypeScript 有两个内置接口:
-
接口
Object 指定了Object 实例的属性,包括从Object.prototype 继承的属性。 -
接口
ObjectConstructor 指定了类Object 的属性。
这些是接口:
interface Object { // (A) constructor: Function; toString(): string; toLocaleString(): string; valueOf(): Object; hasOwnProperty(v: PropertyKey): boolean; isPrototypeOf(v: Object): boolean; propertyIsEnumerable(v: PropertyKey): boolean; } interface ObjectConstructor { /** Invocation via `new` */ new(value?: any): Object; /** Invocation via function calls */ (value?: any): any; readonly prototype: Object; // (B) getPrototypeOf(o: any): any; // ··· } declare var Object: ObjectConstructor; // (C)
观察:
-
我们既有一个名为
Object 的变量(行 C),又有一个名为Object 的类型(行 A)。 -
Object 的直接实例没有自己的属性,因此Object.prototype 也匹配Object (行 B)。
15.3.3?TypeScript 中的object (小写“o”):非原始值
在 TypeScript 中,
15.3.4?TypeScript 中的Object vs. object :原始值
有趣的是,类型
function func1(x: Object) { } func1('abc'); // OK
为什么?原始值具有
> 'abc'.hasOwnProperty === Object.prototype.hasOwnProperty true
相反,
function func2(x: object) { } // @ts-expect-error: Argument of type '"abc"' is not assignable to // parameter of type 'object'. (2345) func2('abc');
15.3.5?TypeScript 中的Object vs. object :不兼容的属性类型
使用类型
// @ts-expect-error: Type '() => number' is not assignable to // type '() => string'. // Type 'number' is not assignable to type 'string'. (2322) const obj1: Object = { toString() { return 123 } };
使用类型
const obj2: object = { toString() { return 123 } };
15.4?对象类型文字和接口
TypeScript 有两种定义非常相似的对象类型的方式:
// Object type literal type ObjType1 = { a: boolean, b: number; c: string, }; // Interface interface ObjType2 { a: boolean, b: number; c: string, }
我们可以使用分号或逗号作为分隔符。允许并且是可选的尾随分隔符。
15.4.1?对象类型文字和接口之间的区别
在本节中,我们将重点介绍对象类型文字和接口之间最重要的区别。
15.4.1.1?内联
对象类型文字可以内联,而接口不能:
// Inlined object type literal: function f1(x: {prop: number}) {} // Referenced interface: function f2(x: ObjectInterface) {} interface ObjectInterface { prop: number; }
15.4.1.2?重复名称
具有重复名称的类型别名是非法的:
// @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300) type PersonAlias = {first: string}; // @ts-expect-error: Duplicate identifier 'PersonAlias'. (2300) type PersonAlias = {last: string};
相反,具有重复名称的接口会合并:
interface PersonInterface { first: string; } interface PersonInterface { last: string; } const jane: PersonInterface = { first: 'Jane', last: 'Doe', };
15.4.1.3?映射类型
对于映射类型(行 A),我们需要使用对象类型文字:
interface Point { x: number; y: number; } type PointCopy1 = { [Key in keyof Point]: Point[Key]; // (A) }; // Syntax error: // interface PointCopy2 { // [Key in keyof Point]: Point[Key]; // };
有关映射类型的更多信息
映射类型超出了本书的范围。有关更多信息,请参见TypeScript 手册。
15.4.1.4?多态this 类型
多态
interface AddsStrings { add(str: string): this; }; class StringBuilder implements AddsStrings { result = ''; add(str: string) { this.result += str; return this; } }
本节的来源
- GitHub 问题“TypeScript:类型 vs. 接口” 由Johannes Ewald
**从现在开始,“接口”意味着“接口或对象类型文字”(除非另有说明)。
15.4.2?TypeScript 中的接口是结构化的
接口是结构化的 - 它们不必被实现才能匹配:
interface Point { x: number; y: number; } const point: Point = {x: 1, y: 2}; // OK
有关此主题的更多信息,请参见[content not included]。
15.4.3?接口和对象类型文字的成员
接口和对象类型文字的主体内的构造被称为它们的成员。这些是最常见的成员:
interface ExampleInterface { // Property signature myProperty: boolean; // Method signature myMethod(str: string): number; // Index signature [key: string]: any; // Call signature (num: number): string; // Construct signature new(str: string): ExampleInstance; } interface ExampleInstance {}
让我们更详细地看看这些成员:
-
属性签名定义属性:
myProperty: boolean;
-
方法签名定义方法:
myMethod(str: string): number;
注意:参数的名称(在本例中为:
str )有助于记录事物的工作原理,但没有其他目的。 -
需要索引签名来描述用作字典的数组或对象。
[key: string]: any;
注意:名称
key 仅用于文档目的。 -
调用签名使接口能够描述函数:
(num: number): string;
-
构造签名使接口能够描述类和构造函数:
new(str: string): ExampleInstance;
属性签名应该是不言自明的。调用签名 和 构造签名 将在本书的后面进行描述。接下来我们将更仔细地看一下方法签名和索引签名。
15.4.4?方法签名
就 TypeScript 的类型系统而言,方法定义和属性的值为函数的属性是等效的:
interface HasMethodDef { simpleMethod(flag: boolean): void; } interface HasFuncProp { simpleMethod: (flag: boolean) => void; } const objWithMethod: HasMethodDef = { simpleMethod(flag: boolean): void {}, }; const objWithMethod2: HasFuncProp = objWithMethod; const objWithOrdinaryFunction: HasMethodDef = { simpleMethod: function (flag: boolean): void {}, }; const objWithOrdinaryFunction2: HasFuncProp = objWithOrdinaryFunction; const objWithArrowFunction: HasMethodDef = { simpleMethod: (flag: boolean): void => {}, }; const objWithArrowFunction2: HasFuncProp = objWithArrowFunction;
我的建议是使用最能表达属性应如何设置的语法。
15.4.5?索引签名:对象作为字典
到目前为止,我们只使用接口来表示具有固定键的对象记录。我们如何表达对象将用作字典的事实?例如:在以下代码片段中,
function translate(dict: TranslationDict, english: string): string { return dict[english]; }
我们使用索引签名(行 A)来表示
interface TranslationDict { [key:string]: string; // (A) } const dict = { 'yes': 'sí', 'no': 'no', 'maybe': 'tal vez', }; assert.equal( translate(dict, 'maybe'), 'tal vez');
15.4.5.1?为索引签名键添加类型
索引签名键必须是
-
不允许使用符号。
-
any 是不允许的。 -
联合类型(例如
string|number )是不允许的。但是,每个接口可以使用多个索引签名。
15.4.5.2?字符串键 vs. 数字键
与纯 JavaScript 一样,TypeScript 的数字属性键是字符串属性键的子集(参见“JavaScript for impatient programmers”)。因此,如果我们既有字符串索引签名又有数字索引签名,则前者的属性类型必须是后者的超类型。以下示例有效,因为
interface StringAndNumberKeys { [key: string]: Object; [key: number]: RegExp; } // %inferred-type: (x: StringAndNumberKeys) => // { str: Object; num: RegExp; } function f(x: StringAndNumberKeys) { return { str: x['abc'], num: x[123] }; }
15.4.5.3?索引签名 vs. 属性签名和方法签名
如果接口中既有索引签名又有属性和/或方法签名,那么索引属性值的类型也必须是属性值和/或方法的超类型。
interface I1 { [key: string]: boolean; // @ts-expect-error: Property 'myProp' of type 'number' is not assignable // to string index type 'boolean'. (2411) myProp: number; // @ts-expect-error: Property 'myMethod' of type '() => string' is not // assignable to string index type 'boolean'. (2411) myMethod(): string; }
相比之下,以下两个接口不会产生错误:
interface I2 { [key: string]: number; myProp: number; } interface I3 { [key: string]: () => string; myMethod(): string; }
15.4.6?接口描述 Object 的实例
所有接口描述的对象都是
在以下示例中,类型为
function f1(x: {}): Object { return x; }
同样,
function f2(x: {}): { toString(): string } { return x; }
15.4.7?多余属性检查:何时允许额外属性?
例如,考虑以下接口:
interface Point { x: number; y: number; }
有两种(等等)方式可以解释此接口:
-
封闭解释:它可以描述所有具有指定类型的属性
.x 和.y 的对象。换句话说:这些对象不能有多余的属性(超出所需的属性)。 -
开放解释:它可以描述所有具有至少属性
.x 和.y 的对象。换句话说:允许多余的属性。
TypeScript 使用两种解释。为了探索它是如何工作的,我们将使用以下函数:
function computeDistance(point: Point) { /*...*/ }
默认情况下,允许多余的属性
const obj = { x: 1, y: 2, z: 3 }; computeDistance(obj); // OK
但是,如果我们直接使用对象文字,则不允许多余的属性:
// @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }' // is not assignable to parameter of type 'Point'. // Object literal may only specify known properties, and 'z' does not // exist in type 'Point'. (2345) computeDistance({ x: 1, y: 2, z: 3 }); // error computeDistance({x: 1, y: 2}); // OK
15.4.7.1?为什么对象文字中禁止多余的属性?
为什么对象文字有更严格的规则?它们可以防止属性键中的拼写错误。我们将使用以下接口来演示这意味着什么。
interface Person { first: string; middle?: string; last: string; } function computeFullName(person: Person) { /*...*/ }
属性
// @ts-expect-error: Argument of type '{ first: string; mdidle: string; // last: string; }' is not assignable to parameter of type 'Person'. // Object literal may only specify known properties, but 'mdidle' // does not exist in type 'Person'. Did you mean to write 'middle'? computeFullName({first: 'Jane', mdidle: 'Cecily', last: 'Doe'});
15.4.7.2?如果对象来自其他地方,为什么允许多余的属性?
这个想法是,如果一个对象来自其他地方,我们可以假设它已经经过审查,不会有任何拼写错误。然后我们可以不那么小心。
如果拼写错误不是问题,我们的目标应该是最大限度地提高灵活性。考虑以下函数:
interface HasYear { year: number; } function getAge(obj: HasYear) { const yearNow = new Date().getFullYear(); return yearNow - obj.year; }
如果不允许大多数传递给
15.4.7.3?空接口允许多余的属性
如果接口为空(或者使用对象类型文字
interface Empty { } interface OneProp { myProp: number; } // @ts-expect-error: Type '{ myProp: number; anotherProp: number; }' is not // assignable to type 'OneProp'. // Object literal may only specify known properties, and // 'anotherProp' does not exist in type 'OneProp'. (2322) const a: OneProp = { myProp: 1, anotherProp: 2 }; const b: Empty = {myProp: 1, anotherProp: 2}; // OK
15.4.7.4?仅匹配没有属性的对象
如果我们想要强制对象没有属性,我们可以使用以下技巧(来源:Geoff Goodman):
interface WithoutProperties { [key: string]: never; } // @ts-expect-error: Type 'number' is not assignable to type 'never'. (2322) const a: WithoutProperties = { prop: 1 }; const b: WithoutProperties = {}; // OK
15.4.7.5?允许对象文字中的多余属性
如果我们想要允许对象文字中的多余属性怎么办?例如,考虑接口
interface Point { x: number; y: number; } function computeDistance1(point: Point) { /*...*/ } // @ts-expect-error: Argument of type '{ x: number; y: number; z: number; }' // is not assignable to parameter of type 'Point'. // Object literal may only specify known properties, and 'z' does not // exist in type 'Point'. (2345) computeDistance1({ x: 1, y: 2, z: 3 });
一种选择是将对象文字分配给一个中间变量:
const obj = { x: 1, y: 2, z: 3 }; computeDistance1(obj);
第二种选择是使用类型断言:
computeDistance1({ x: 1, y: 2, z: 3 } as Point); // OK
第三种选择是重写
function computeDistance2<P extends Point>(point: P) { /*...*/ } computeDistance2({ x: 1, y: 2, z: 3 }); // OK
第四种选择是扩展接口
interface PointEtc extends Point { [key: string]: any; } function computeDistance3(point: PointEtc) { /*...*/ } computeDistance3({ x: 1, y: 2, z: 3 }); // OK
我们将继续讨论两个示例,其中 TypeScript 不允许多余的属性是一个问题。
15.4.7.5.1?允许多余的属性:示例Incrementor
在这个例子中,我们想要实现一个
interface Incrementor { inc(): void } function createIncrementor(start = 0): Incrementor { return { // @ts-expect-error: Type '{ counter: number; inc(): void; }' is not // assignable to type 'Incrementor'. // Object literal may only specify known properties, and // 'counter' does not exist in type 'Incrementor'. (2322) counter: start, inc() { // @ts-expect-error: Property 'counter' does not exist on type // 'Incrementor'. (2339) this.counter++; }, }; }
然而,即使使用类型断言,仍然存在一个类型错误:
function createIncrementor2(start = 0): Incrementor { return { counter: start, inc() { // @ts-expect-error: Property 'counter' does not exist on type // 'Incrementor'. (2339) this.counter++; }, } as Incrementor; }
我们可以在接口
function createIncrementor3(start = 0): Incrementor { const incrementor = { counter: start, inc() { this.counter++; }, }; return incrementor; }
15.4.7.5.2?允许多余的属性:示例.dateStr
以下比较函数可用于对具有属性
function compareDateStrings( a: {dateStr: string}, b: {dateStr: string}) { if (a.dateStr < b.dateStr) { return +1; } else if (a.dateStr > b.dateStr) { return -1; } else { return 0; } }
例如,在单元测试中,我们可能希望直接使用对象文字调用此函数。TypeScript 不允许我们这样做,我们需要使用其中一种解决方法。
15.5?类型推断
这些是 TypeScript 通过各种方式创建的对象推断的类型:
// %inferred-type: Object const obj1 = new Object(); // %inferred-type: any const obj2 = Object.create(null); // %inferred-type: {} const obj3 = {}; // %inferred-type: { prop: number; } const obj4 = {prop: 123}; // %inferred-type: object const obj5 = Reflect.getPrototypeOf({});
原则上,
15.6?接口的其他特性
15.6.1?可选属性
如果我们在属性名称后面加上问号(
interface Name { first: string; middle?: string; last: string; }
因此,省略该属性是可以的(A 行):
const john: Name = {first: 'Doe', last: 'Doe'}; // (A) const jane: Name = {first: 'Jane', middle: 'Cecily', last: 'Doe'};
15.6.1.1?可选 vs. undefined|string
interface Interf { prop1?: string; prop2: undefined | string; }
可选属性可以做
const obj1: Interf = { prop1: undefined, prop2: undefined };
然而,只有
const obj2: Interf = { prop2: undefined }; // @ts-expect-error: Property 'prop2' is missing in type '{}' but required // in type 'Interf'. (2741) const obj3: Interf = { };
诸如
15.6.2?只读属性
在下面的例子中,属性
interface MyInterface { readonly prop: number; }
因此,我们可以读取它,但我们不能更改它:
const obj: MyInterface = { prop: 1, }; console.log(obj.prop); // OK // @ts-expect-error: Cannot assign to 'prop' because it is a read-only // property. (2540) obj.prop = 2;
15.7?JavaScript 的原型链和 TypeScript 的类型
TypeScript 不区分自有属性和继承属性。它们都被简单地视为属性。
interface MyInterface { toString(): string; // inherited property prop: number; // own property } const obj: MyInterface = { // OK prop: 123, };
这种方法的缺点是,JavaScript 中的一些现象无法通过 TypeScript 的类型系统来描述。好处是类型系统更简单。
15.8?本章的来源
-
TypeScript 手册
-
TypeScript 语言规范
评论
十六、TypeScript 中的类定义
原文:
exploringjs.com/tackling-ts/ch_class-definitions.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
16.1?速查表:纯 JavaScript 中的类
-
16.1.1?类的基本成员
-
16.1.2?修饰符:
static -
16.1.3?类似修饰符的名称前缀:
# (私有) -
16.1.4?访问器的修饰符:
get (getter)和set (setter) -
16.1.5?方法的修饰符:
* (生成器) -
16.1.6?方法的修饰符:
async -
16.1.7?计算类成员名称
-
16.1.8?修饰符的组合
-
16.1.9?底层原理
-
16.1.10?纯 JavaScript 中类定义的更多信息
-
-
16.2?TypeScript 中的非公共数据槽
-
16.2.1?私有属性
-
16.2.2?私有字段
-
16.2.3?私有属性 vs. 私有字段
-
16.2.4?受保护的属性
-
-
16.3?私有构造函数 (ch_class-definitions.html#private-constructors)
-
16.4?初始化实例属性
-
16.4.1?严格的属性初始化
-
16.4.2?使构造函数参数
public 、private 或protected
-
-
16.5?抽象类
在本章中,我们将研究 TypeScript 中类定义的工作方式:
-
首先,我们快速查看纯 JavaScript 中类定义的特性。
-
然后我们探讨 TypeScript 为此带来了哪些新增内容。
16.1?速查表:纯 JavaScript 中的类
本节是关于纯 JavaScript 中类定义的速查表。
16.1.1?类的基本成员
class OtherClass {} class MyClass1 extends OtherClass { publicInstanceField = 1; constructor() { super(); } publicPrototypeMethod() { return 2; } } const inst1 = new MyClass1(); assert.equal(inst1.publicInstanceField, 1); assert.equal(inst1.publicPrototypeMethod(), 2);
接下来的部分是关于修饰符的
最后,有一张表显示了修饰符如何组合。
16.1.2?修饰符:static
class MyClass2 { static staticPublicField = 1; static staticPublicMethod() { return 2; } } assert.equal(MyClass2.staticPublicField, 1); assert.equal(MyClass2.staticPublicMethod(), 2);
16.1.3?类似修饰符的名称前缀:# (私有)
class MyClass3 { #privateField = 1; #privateMethod() { return 2; } static accessPrivateMembers() { // Private members can only be accessed from inside class definitions const inst3 = new MyClass3(); assert.equal(inst3.#privateField, 1); assert.equal(inst3.#privateMethod(), 2); } } MyClass3.accessPrivateMembers();
JavaScript 警告:
-
目前对私有方法的支持非常有限。
-
私有字段有更广泛的支持,但也有限制。
TypeScript 自 3.8 版本以来一直支持私有字段,但目前不支持私有方法。
16.1.4?访问器的修饰符:get (getter)和set (setter)
大致上,访问器是通过访问属性调用的方法。有两种类型的访问器:getter 和 setter。
class MyClass5 { #name = 'Rumpelstiltskin'; /** Prototype getter */ get name() { return this.#name; } /** Prototype setter */ set name(value) { this.#name = value; } } const inst5 = new MyClass5(); assert.equal(inst5.name, 'Rumpelstiltskin'); // getter inst5.name = 'Queen'; // setter assert.equal(inst5.name, 'Queen'); // getter
16.1.5?方法的修饰符:* (生成器)
class MyClass6 { * publicPrototypeGeneratorMethod() { yield 'hello'; yield 'world'; } } const inst6 = new MyClass6(); assert.deepEqual( [...inst6.publicPrototypeGeneratorMethod()], ['hello', 'world']);
16.1.6?方法的修饰符:async
class MyClass7 { async publicPrototypeAsyncMethod() { const result = await Promise.resolve('abc'); return result + result; } } const inst7 = new MyClass7(); inst7.publicPrototypeAsyncMethod() .then(result => assert.equal(result, 'abcabc'));
16.1.7?计算类成员名称
const publicInstanceFieldKey = Symbol('publicInstanceFieldKey'); const publicPrototypeMethodKey = Symbol('publicPrototypeMethodKey'); class MyClass8 { [publicInstanceFieldKey] = 1; [publicPrototypeMethodKey]() { return 2; } } const inst8 = new MyClass8(); assert.equal(inst8[publicInstanceFieldKey], 1); assert.equal(inst8[publicPrototypeMethodKey](), 2);
评论:
-
此功能的主要用例是诸如
Symbol.iterator 之类的符号。但是任何表达式都可以在方括号内使用。 -
我们可以计算字段、方法和访问器的名称。
-
我们无法计算私有成员的名称(这些名称始终是固定的)。
16.1.8?修饰符的组合
字段(没有级别意味着构造存在于实例级别):
级别 | 可见性 |
---|---|
(实例) | |
(实例) | |
方法(没有级别表示构造存在于原型级别):
级别 | 访问器 | 异步 | 生成器 | 可见性 |
---|---|---|---|---|
(原型) | ||||
(原型) | ||||
(原型) | ||||
(原型) | ||||
(原型) | ||||
(原型) | ||||
(与原型相关) | ||||
(与原型相关) | ||||
(与原型相关) | ||||
(与原型相关) | ||||
(与原型相关) | ||||
(与原型相关) | ||||
方法的限制:
- 访问器不能是异步的或生成器。
16.1.9?底层
重要的是要记住,对于类,有两条原型对象链:
-
以一个实例开始的实例链。
-
从该实例的类开始的静态链。
考虑以下纯 JavaScript 示例:
class ClassA { static staticMthdA() {} constructor(instPropA) { this.instPropA = instPropA; } prototypeMthdA() {} } class ClassB extends ClassA { static staticMthdB() {} constructor(instPropA, instPropB) { super(instPropA); this.instPropB = instPropB; } prototypeMthdB() {} } const instB = new ClassB(0, 1);
图 1 显示了由
)
图 1:
16.1.10?纯 JavaScript 中类定义的更多信息
-
公共字段、私有字段、私有方法/获取器/设置器(博客文章)
-
所有剩余的 JavaScript 类特性(“JavaScript for impatient programming”中的章节)
16.2?TypeScript 中的非公共数据槽
在 TypeScript 中,默认情况下,所有数据槽都是公共属性。有两种方法可以保持数据私有:
-
私有属性
-
私有字段
我们接下来会看两者。
请注意,TypeScript 目前不支持私有方法。
16.2.1?私有属性
私有属性是 TypeScript 专有的(静态)特性。通过在关键字
class PersonPrivateProperty { private name: string; // (A) constructor(name: string) { this.name = name; } sayHello() { return `Hello ${this.name}!`; } }
现在,如果我们在错误的范围内访问该属性,我们会得到编译时错误(A 行):
const john = new PersonPrivateProperty('John'); assert.equal( john.sayHello(), 'Hello John!'); // @ts-expect-error: Property 'name' is private and only accessible // within class 'PersonPrivateProperty'. (2341) john.name; // (A)
然而,
assert.deepEqual( Object.keys(john), ['name']);
当我们查看类编译成的 JavaScript 代码时,我们还可以看到私有属性在运行时不受保护:
class PersonPrivateProperty { constructor(name) { this.name = name; } sayHello() { return `Hello ${this.name}!`; } }
16.2.2?私有字段
私有字段是 TypeScript 自 3.8 版本以来支持的新 JavaScript 特性:
class PersonPrivateField { #name: string; constructor(name: string) { this.#name = name; } sayHello() { return `Hello ${this.#name}!`; } }
这个版本的
const john = new PersonPrivateField('John'); assert.equal( john.sayHello(), 'Hello John!');
然而,这次,数据完全封装起来了。在类外部使用私有字段语法甚至是 JavaScript 语法错误。这就是为什么我们必须在 A 行使用
assert.throws( () => eval('john.#name'), // (A) { name: 'SyntaxError', message: "Private field '#name' must be declared in " + "an enclosing class", }); assert.deepEqual( Object.keys(john), []);
编译结果现在更加复杂(稍微简化):
var __classPrivateFieldSet = function (receiver, privateMap, value) { if (!privateMap.has(receiver)) { throw new TypeError( 'attempted to set private field on non-instance'); } privateMap.set(receiver, value); return value; }; // Omitted: __classPrivateFieldGet var _name = new WeakMap(); class Person { constructor(name) { // Add an entry for this instance to _name _name.set(this, void 0); // Now we can use the helper function: __classPrivateFieldSet(this, _name, name); } // ··· }
这段代码使用了一个保持实例数据私有的常见技术:
-
每个 WeakMap 实现一个私有字段。
-
它将每个实例与一个私有数据关联起来。
关于这个主题的更多信息:请参阅“JavaScript for impatient programmers”。
16.2.3?私有属性 vs. 私有字段
-
私有属性的缺点:
-
我们不能在子类中重用私有属性的名称(因为属性在运行时不是私有的)。
-
在运行时没有封装。
-
-
私有属性的优点:
- 客户端可以规避封装并访问私有属性。如果有人需要解决 bug,这可能是有用的。换句话说:数据完全封装有利有弊。
16.2.4 受保护的属性
私有字段和私有属性不能在子类中访问(A 行):
class PrivatePerson { private name: string; constructor(name: string) { this.name = name; } sayHello() { return `Hello ${this.name}!`; } } class PrivateEmployee extends PrivatePerson { private company: string; constructor(name: string, company: string) { super(name); this.company = company; } sayHello() { // @ts-expect-error: Property 'name' is private and only // accessible within class 'PrivatePerson'. (2341) return `Hello ${this.name} from ${this.company}!`; // (A) } }
我们可以通过在 A 行将
class ProtectedPerson { protected name: string; // (A) constructor(name: string) { this.name = name; } sayHello() { return `Hello ${this.name}!`; } } class ProtectedEmployee extends ProtectedPerson { protected company: string; // (B) constructor(name: string, company: string) { super(name); this.company = company; } sayHello() { return `Hello ${this.name} from ${this.company}!`; // OK } }
16.3 私有构造函数
构造函数也可以是私有的。当我们有静态工厂方法并且希望客户端始终使用这些方法而不是直接使用构造函数时,这是很有用的。静态方法可以访问私有类成员,这就是为什么工厂方法仍然可以使用构造函数的原因。
在以下代码中,有一个静态工厂方法
class DataContainer { #data: string; static async create() { const data = await Promise.resolve('downloaded'); // (A) return new this(data); } private constructor(data: string) { this.#data = data; } getData() { return 'DATA: '+this.#data; } } DataContainer.create() .then(dc => assert.equal( dc.getData(), 'DATA: downloaded'));
在实际代码中,我们会使用
私有构造函数防止
16.4 初始化实例属性
16.4.1 严格的属性初始化
如果编译器设置
-
要么通过构造函数中的赋值:
class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } }
-
或通过属性声明的初始化程序:
class Point { x = 0; y = 0; // No constructor needed }
然而,有时我们以 TypeScript 无法识别的方式初始化属性。然后我们可以使用感叹号(确定赋值断言)来关闭 TypeScript 的警告(A 行和 B 行):
class Point { x!: number; // (A) y!: number; // (B) constructor() { this.initProperties(); } initProperties() { this.x = 0; this.y = 0; } }
16.4.1.1 例子:通过对象设置实例属性
在下面的示例中,我们还需要确定赋值断言。在这里,我们通过构造函数参数
class CompilerError implements CompilerErrorProps { // (A) line!: number; description!: string; constructor(props: CompilerErrorProps) { Object.assign(this, props); // (B) } } // Helper interface for the parameter properties interface CompilerErrorProps { line: number, description: string, } // Using the class: const err = new CompilerError({ line: 123, description: 'Unexpected token', });
注:
-
在 B 行,我们初始化了所有属性:我们使用
Object.assign() 将参数props 的属性复制到this 中。 -
在 A 行,
implements 确保类声明了接口CompilerErrorProps 中的所有属性。
16.4.2 使构造函数参数public ,private 或protected
如果我们对构造函数参数使用关键字
-
它声明了一个具有相同名称的公共实例属性。
-
它将参数分配给该实例属性。
因此,以下两个类是等价的:
class Point1 { constructor(public x: number, public y: number) { } } class Point2 { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } }
如果我们使用
16.5 抽象类
在 TypeScript 中,两个构造可以是抽象的:
-
抽象类不能被实例化。只有它的子类可以——如果它们自己不是抽象的。
-
抽象方法没有实现,只有类型签名。每个具体的子类必须具有相同名称和兼容类型签名的具体方法。
- 如果一个类有任何抽象方法,它也必须是抽象的。
以下代码演示了抽象类和方法。
一方面,有一个抽象的超类
class StringBuilder { string = ''; add(str: string) { this.string += str; } } abstract class Printable { toString() { const out = new StringBuilder(); this.print(out); return out.string; } abstract print(out: StringBuilder): void; }
另一方面,有具体的子类
class Entries extends Printable { entries: Entry[]; constructor(entries: Entry[]) { super(); this.entries = entries; } print(out: StringBuilder): void { for (const entry of this.entries) { entry.print(out); } } } class Entry extends Printable { key: string; value: string; constructor(key: string, value: string) { super(); this.key = key; this.value = value; } print(out: StringBuilder): void { out.add(this.key); out.add(': '); out.add(this.value); out.add(' '); } }
最后,这是我们使用
const entries = new Entries([ new Entry('accept-ranges', 'bytes'), new Entry('content-length', '6518'), ]); assert.equal( entries.toString(), 'accept-ranges: bytes content-length: 6518 ');
关于抽象类的注释:
-
抽象类可以被视为具有一些成员已经有实现的接口。
-
虽然一个类可以实现多个接口,但它最多只能扩展一个抽象类。
-
“抽象性”只存在于编译时。在运行时,抽象类是普通类,抽象方法不存在(因为它们只提供编译时信息)。
-
抽象类可以被视为模板,其中每个抽象方法都是必须由子类填写(实现)的空白。
评论
十七、与类相关的类型
原文:
exploringjs.com/tackling-ts/ch_class-related-types.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
17.1?类的两个原型链
-
17.2?类的实例接口
-
17.3?类的接口
-
17.3.1?示例:从 JSON 转换和转换为 JSON
-
17.3.2?示例:TypeScript 内置接口用于类
Object 及其实例
-
-
17.4?类作为类型
- 17.4.1?陷阱:类的结构工作,而不是名义上的
-
17.5?进一步阅读
在这一章关于 TypeScript 的内容中,我们研究与类及其实例相关的类型。
17.1?类的两个原型链
考虑这个类:
class Counter extends Object { static createZero() { return new Counter(0); } value: number; constructor(value: number) { super(); this.value = value; } increment() { this.value++; } } // Static method const myCounter = Counter.createZero(); assert.ok(myCounter instanceof Counter); assert.equal(myCounter.value, 0); // Instance method myCounter.increment(); assert.equal(myCounter.value, 1);
图 2:类
图 2 中的图表显示了类
-
类(左侧):静态原型链由组成类
Counter 的对象组成。类Counter 的原型对象是它的超类Object 。 -
实例(右侧):实例原型链由组成实例
myCounter 的对象组成。链以实例myCounter 开始,然后是Counter.prototype (其中包含类Counter 的原型方法)和Object.prototype (其中包含类Object 的原型方法)。
在本章中,我们首先探讨实例对象,然后是作为对象的类。
17.2?类的实例接口
接口指定对象提供的服务。例如:
interface CountingService { value: number; increment(): void; }
TypeScript 的接口是结构化的:为了使一个对象实现一个接口,它只需要具有正确类型的正确属性。我们可以在下面的例子中看到这一点:
const myCounter2: CountingService = new Counter(3);
结构接口很方便,因为我们甚至可以为已经存在的对象创建接口(即,在事后引入它们)。
如果我们提前知道一个对象必须实现一个给定的接口,通常最好提前检查它是否实现了,以避免后来的意外。我们可以通过
class Counter implements CountingService { // ··· };
注:
-
TypeScript 不区分继承的属性(如
.increment )和自有属性(如.value )。 -
另外,接口忽略私有属性,并且不能通过接口指定私有属性。这是可以预料的,因为私有数据仅供内部使用。
17.3?类的接口
类本身也是对象(函数)。因此,我们可以使用接口来指定它们的属性。这里的主要用例是描述对象的工厂。下一节给出了一个例子。
17.3.1?示例:从 JSON 转换和转换为 JSON
以下两个接口可用于支持其实例从 JSON 转换和转换为 JSON 的类:
// Converting JSON to instances interface JsonStatic { fromJson(json: any): JsonInstance; } // Converting instances to JSON interface JsonInstance { toJson(): any; }
我们在下面的代码中使用这些接口:
class Person implements JsonInstance { static fromJson(json: any): Person { if (typeof json !== 'string') { throw new TypeError(json); } return new Person(json); } name: string; constructor(name: string) { this.name = name; } toJson(): any { return this.name; } }
这是我们可以立即检查类
// Assign the class to a type-annotated variable const personImplementsJsonStatic: JsonStatic = Person;
以下方式进行此检查可能看起来是一个好主意:
const Person: JsonStatic = class implements JsonInstance { // ··· };
然而,这并不真正起作用:
-
我们不能
new -callPerson ,因为JsonStatic 没有构造签名。 -
如果
Person 具有超出.fromJson() 的静态属性,TypeScript 不会让我们访问它们。
17.3.2 示例:TypeScript 的内置接口用于类Object 及其实例
看一下 TypeScript 内置类型是很有启发性的:
一方面,接口
/** * Provides functionality common to all JavaScript objects. */ declare var Object: ObjectConstructor; interface ObjectConstructor { new(value?: any): Object; (): any; (value: any): any; /** A reference to the prototype for a class of objects. */ readonly prototype: Object; /** * Returns the prototype of an object. * @param o The object that references the prototype. */ getPrototypeOf(o: any): any; }
另一方面,接口
interface Object { /** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */ constructor: Function; /** Returns a string representation of an object. */ toString(): string; }
名称
-
在动态级别,对于一个全局变量。
-
在静态级别,对于一个类型。
17.4 类作为类型
考虑以下类:
class Color { name: string; constructor(name: string) { this.name = name; } }
这个类定义创建了两个东西。
首先,一个名为
assert.equal( typeof Color, 'function')
其次,一个名为
const green: Color = new Color('green');
这里有证据表明
interface RgbColor extends Color { rgbValue: [number, number, number]; }
17.4.1 陷阱:类在结构上工作,而不是名义上。
不过有一个陷阱:使用
class Color { name: string; constructor(name: string) { this.name = name; } } class Person { name: string; constructor(name: string) { this.name = name; } } const person: Person = new Person('Jane'); const color: Color = person; // (A)
为什么 TypeScript 在 A 行没有抱怨呢?这是由于结构类型:
17.4.1.1 关闭结构类型
我们可以通过添加私有属性使这两组对象不兼容:
class Color { name: string; private branded = true; constructor(name: string) { this.name = name; } } class Person { name: string; private branded = true; constructor(name: string) { this.name = name; } } const person: Person = new Person('Jane'); // @ts-expect-error: Type 'Person' is not assignable to type 'Color'. // Types have separate declarations of a private property // 'branded'. (2322) const color: Color = person;
这种情况下,私有属性关闭了结构类型。
17.5 进一步阅读
- 章节“原型链和类” 在“JavaScript for impatient programmers”
评论
十八、类作为值的类型
原文:
exploringjs.com/tackling-ts/ch_classes-as-values.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
18.1?特定类的类型
-
18.2?类型操作符
typeof -
18.2.1?构造函数类型文本
-
18.2.2?带有构造签名的对象类型文本
-
-
18.3?类的通用类型:
Class<T> -
18.3.1?示例:创建实例
-
18.3.2?示例:带有运行时检查的类型转换
-
18.3.3?示例:在运行时类型安全的映射
-
18.3.4?陷阱:
Class<T> 不匹配抽象类
-
在本章中,我们探讨了类作为值:
-
我们应该为这些值使用什么类型?
-
这些类型的用例是什么?
18.1?特定类的类型
考虑以下类:
class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } }
这个函数接受一个类并创建它的一个实例:
function createPoint(PointClass: ???, x: number, y: number) { return new PointClass(x, y); }
如果我们希望参数
18.2?类型操作符typeof
在§7.7“两种语言级别:动态 vs. 静态”中,我们探讨了 TypeScript 的两种语言级别:
-
动态级别:JavaScript(代码和值)
-
静态级别:TypeScript(静态类型)
类
-
构造函数
Point -
接口
Point 用于Point 的实例
根据我们提到
相反,我们需要使用类型操作符
function createPoint(PointClass: typeof Point, x: number, y: number) { // (A) return new PointClass(x, y); } // %inferred-type: Point const point = createPoint(Point, 3, 6); assert.ok(point instanceof Point);
18.2.1?构造函数类型文本
构造函数类型文本是一个带有前缀
function createPoint( PointClass: new (x: number, y: number) => Point, // (A) x: number, y: number ) { return new PointClass(x, y); }
18.2.2?带有构造签名的对象类型文本
回想一下接口和对象文本类型(OLT)的成员包括方法签名和调用签名。调用签名使接口和 OLT 能够描述函数。
同样,构造签名使接口和 OLT 能够描述构造函数。它们看起来像带有前缀
function createPoint( PointClass: {new (x: number, y: number): Point}, x: number, y: number ) { return new PointClass(x, y); }
18.3?类的通用类型:Class<T>
根据我们所学的知识,我们现在可以创建一个类的通用类型作为值 - 通过引入类型参数
type Class<T> = new (...args: any[]) => T;
除了类型别名,我们还可以使用接口:
interface Class<T> { new(...args: any[]): T; }
18.3.1?示例:创建实例
function createInstance<T>(AnyClass: Class<T>, ...args: any[]): T { return new AnyClass(...args); }
class Person { constructor(public name: string) {} } // %inferred-type: Person const jane = createInstance(Person, 'Jane');
18.3.2?示例:带有运行时检查的类型转换
我们可以使用
function cast<T>(AnyClass: Class<T>, obj: any): T { if (! (obj instanceof AnyClass)) { throw new Error(`Not an instance of ${AnyClass.name}: ${obj}`) } return obj; }
通过
function parseObject(jsonObjectStr: string): Object { // %inferred-type: any const parsed = JSON.parse(jsonObjectStr); return cast(Object, parsed); }
18.3.3?示例:在运行时类型安全的映射
class TypeSafeMap { #data = new Map<any, any>(); get<T>(key: Class<T>) { const value = this.#data.get(key); return cast(key, value); } set<T>(key: Class<T>, value: T): this { cast(key, value); // runtime check this.#data.set(key, value); return this; } has(key: any) { return this.#data.has(key); } }
这是
const map = new TypeSafeMap(); map.set(RegExp, /abc/); // %inferred-type: RegExp const re = map.get(RegExp); // Static and dynamic error! assert.throws( // @ts-expect-error: Argument of type '"abc"' is not assignable // to parameter of type 'Date'. () => map.set(Date, 'abc'));
18.3.4?陷阱:Class<T> 与抽象类不匹配
当期望
abstract class Shape { } class Circle extends Shape { // ··· } // @ts-expect-error: Type 'typeof Shape' is not assignable to type // 'Class<Shape>'. // Cannot assign an abstract constructor type to a non-abstract // constructor type. (2322) const shapeClasses1: Array<Class<Shape>> = [Circle, Shape];
为什么呢?原因是构造函数类型文字和构造签名应该只用于实际可以被
这是一个变通方法:
type Class2<T> = Function & {prototype: T}; const shapeClasses2: Array<Class2<Shape>> = [Circle, Shape];
这种方法的缺点:
-
稍微令人困惑。
-
具有此类型的值不能用于
instanceof 检查(作为右操作数)。
评论
十九、数组的类型化
原文:
exploringjs.com/tackling-ts/ch_typing-arrays.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
19.1?数组的角色
-
19.2?数组的类型化方式
-
19.2.1?数组角色“列表”:数组类型字面量 vs. 接口类型
Array -
19.2.2?数组角色“元组”:元组类型字面量
-
19.2.3?也是类似数组的对象:带有索引签名的接口
-
-
19.3?陷阱:类型推断并不总是正确获取数组类型
-
19.3.1?推断数组类型很困难
-
19.3.2?非空数组字面量的类型推断
-
19.3.3?空数组字面量的类型推断
-
19.3.4?对数组和类型推断进行
const 断言
-
-
19.4?陷阱:TypeScript 假设索引永远不会越界
在本章中,我们将讨论如何在 TypeScript 中为数组添加类型。
19.1?数组的角色
数组在 JavaScript 中可以扮演以下角色(单一或混合):
-
列表:所有元素具有相同的类型。数组的长度不同。
-
元组:数组的长度是固定的。元素不一定具有相同的类型。
TypeScript 通过提供各种数组类型化的方式来适应这两种角色。我们将在下面看看这些方式。
19.2?数组的类型化方式
19.2.1?数组角色“列表”:数组类型字面量 vs. 接口类型Array
数组类型字面量由元素类型后跟
// Each Array element has the type `string`: const myStringArray: string[] = ['fee', 'fi', 'fo', 'fum'];
数组类型字面量是使用全局通用接口类型
const myStringArray: Array<string> = ['fee', 'fi', 'fo', 'fum'];
如果元素类型更复杂,我们需要使用数组类型字面量的括号:
(number|string)[] (() => boolean)[]
在这种情况下,通用类型
Array<number|string> Array<() => boolean>
19.2.2?数组角色“元组”:元组类型字面量
如果数组的长度固定,并且每个元素具有不同的固定类型,取决于其位置,则我们可以使用元组类型字面量,例如
const yes: [string, string, boolean] = ['oui', 'sí', true];
19.2.3?也是类似数组的对象:带有索引签名的接口
如果一个接口只有一个索引签名,我们可以用它来表示数组:
interface StringArray { [index: number]: string; } const strArr: StringArray = ['Huey', 'Dewey', 'Louie'];
具有索引签名和属性签名的接口仅适用于对象(因为索引元素和属性需要同时定义):
interface FirstNamesAndLastName { [index: number]: string; lastName: string; } const ducks: FirstNamesAndLastName = { 0: 'Huey', 1: 'Dewey', 2: 'Louie', lastName: 'Duck', };
19.3?陷阱:类型推断并不总是正确获取数组类型
19.3.1?推断数组类型很困难
由于数组的两种角色,TypeScript 不可能总是猜对类型。例如,考虑以下分配给变量
const fields: Fields = [ ['first', 'string', true], ['last', 'string', true], ['age', 'number', false], ];
type Fields = Array<[string, string, boolean]>;
type Fields = Array<[string, ('string'|'number'), boolean]>;
type Fields = Array<Array<string|boolean>>;
type Fields = [ [string, string, boolean], [string, string, boolean], [string, string, boolean], ];
type Fields = [ [string, 'string', boolean], [string, 'string', boolean], [string, 'number', boolean], ];
type Fields = [ Array<string|boolean>, Array<string|boolean>, Array<string|boolean>, ];
19.3.2?非空数组字面量的类型推断
当我们使用非空数组字面量时,TypeScript 的默认值是推断列表类型(而不是元组类型):
// %inferred-type: (string | number)[] const arr = [123, 'abc'];
然而,这并不总是我们想要的:
function func(p: [number, number]) { return p; } // %inferred-type: number[] const pair1 = [1, 2]; // @ts-expect-error: Argument of type 'number[]' is not assignable to // parameter of type '[number, number]'. [...] func(pair1);
我们可以通过在
const pair2: [number, number] = [1, 2]; func(pair2); // OK
19.3.3?空数组字面量的类型推断
如果我们用空数组字面量初始化一个变量,那么 TypeScript 最初会推断类型为
// %inferred-type: any[] const arr1 = []; arr1.push(123); // %inferred-type: number[] arr1; arr1.push('abc'); // %inferred-type: (string | number)[] arr1;
请注意,初始推断类型不受后续发生的影响。
如果我们使用赋值而不是
// %inferred-type: any[] const arr1 = []; arr1[0] = 123; // %inferred-type: number[] arr1; arr1[1] = 'abc'; // %inferred-type: (string | number)[] arr1;
相反,如果数组文字至少有一个元素,则元素类型是固定的,以后不会改变:
// %inferred-type: number[] const arr = [123]; // @ts-expect-error: Argument of type '"abc"' is not assignable to // parameter of type 'number'. (2345) arr.push('abc');
19.3.4?数组和类型推断的const 断言
我们可以在数组文字后缀中使用a
// %inferred-type: readonly ["igneous", "metamorphic", "sedimentary"] const rockCategories = ['igneous', 'metamorphic', 'sedimentary'] as const;
我们声明
-
数组变为
readonly - 我们不能使用改变它的操作:// @ts-expect-error: Property 'push' does not exist on type // 'readonly ["igneous", "metamorphic", "sedimentary"]'. (2339) rockCategories.push('sand');
-
TypeScript 推断出一个元组。比较:
// %inferred-type: string[] const rockCategories2 = ['igneous', 'metamorphic', 'sedimentary'];
-
TypeScript 推断出文字类型(例如
"igneous" 等)而不是更一般的类型。也就是说,推断的元组类型不是[string, string, string] 。
以下是使用和不使用
// %inferred-type: readonly [1, 2, 3, 4] const numbers1 = [1, 2, 3, 4] as const; // %inferred-type: number[] const numbers2 = [1, 2, 3, 4]; // %inferred-type: readonly [true, "abc"] const booleanAndString1 = [true, 'abc'] as const; // %inferred-type: (string | boolean)[] const booleanAndString2 = [true, 'abc'];
19.3.4.1?const 断言的潜在陷阱
首先,推断类型尽可能狭窄。这对于使用
let arr = [1, 2] as const; arr = [1, 2]; // OK // @ts-expect-error: Type '3' is not assignable to type '2'. (2322) arr = [1, 3];
其次,通过
let arr = [1, 2] as const; // @ts-expect-error: Cannot assign to '1' because it is a read-only // property. (2540) arr[1] = 3;
这既不是优势也不是劣势,但我们需要意识到这一点。
19.4?陷阱:TypeScript 假设索引永远不会超出范围
每当我们通过索引访问数组元素时,TypeScript 总是假设索引在范围内(A 行):
const messages: string[] = ['Hello']; // %inferred-type: string const message = messages[3]; // (A)
由于这个假设,
如果我们使用元组类型,我们会得到一个错误:
const messages: [string] = ['Hello']; // @ts-expect-error: Tuple type '[string]' of length '1' has no element // at index '1'. (2493) const message = messages[1];
评论
二十、函数类型
原文:
exploringjs.com/tackling-ts/ch_typing-functions.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
20.1?定义静态类型函数
-
20.1.1?函数声明
-
20.1.2?箭头函数
-
-
20.2?函数类型
-
20.2.1?函数类型签名
-
20.2.2?带有调用签名的接口
-
20.2.3?检查可调用值是否匹配函数类型
-
-
20.3?参数
-
20.3.1?何时必须对参数进行类型注释?
-
20.3.2?可选参数
-
20.3.3?剩余参数
-
20.3.4?命名参数
-
20.3.5?
this 作为参数(高级)
-
-
20.4?重载(高级)
-
20.4.1?重载函数声明
-
20.4.2?通过接口进行重载
-
20.4.3?基于字符串参数的重载(事件处理等)
-
20.4.4?重载方法
-
-
20.5?可赋值性(高级)
-
20.5.1?可赋值性规则
-
20.5.2?函数赋值规则的后果
-
-
20.6?进一步阅读和本章的来源
本章探讨了 TypeScript 中函数的静态类型。
在本章中,“函数”指的是“函数或方法或构造函数”
在本章中,关于函数的大部分内容(特别是参数处理方面)也适用于方法和构造函数。
20.1?定义静态类型函数
20.1.1?函数声明
这是 TypeScript 中函数声明的一个例子:
function repeat1(str: string, times: number): string { // (A) return str.repeat(times); } assert.equal( repeat1('*', 5), '*****');
-
参数:如果编译器选项
--noImplicitAny 打开(如果--strict 打开),则每个参数的类型必须是可推断的或明确指定的。(我们稍后会更仔细地看一下推断。)在这种情况下,无法进行推断,这就是为什么str 和times 有类型注释的原因。 -
返回值:默认情况下,函数的返回类型是推断的。通常这已经足够好了。在这种情况下,我们选择明确指定
repeat1() 的返回类型为string (A 行中的最后一个类型注释)。
20.1.2?箭头函数
const repeat2 = (str: string, times: number): string => { return str.repeat(times); };
在这种情况下,我们也可以使用表达式体:
const repeat3 = (str: string, times: number): string => str.repeat(times);
20.2?函数类型
20.2.1?函数类型签名
我们可以通过函数类型签名为函数定义类型:
type Repeat = (str: string, times: number) => string;
这种类型的函数名为
-
两个类型分别为
string 和number 的参数。我们需要在函数类型签名中命名参数,但在检查两个函数类型是否兼容时,名称会被忽略。 -
返回类型为
string 。请注意,这次类型是由箭头分隔的,不能省略。
这种类型匹配更多的函数。我们将在本章后面探讨可赋值性的规则时学习到更多信息。
20.2.2?具有调用签名的接口
我们还可以使用接口来定义函数类型:
interface Repeat { (str: string, times: number): string; // (A) }
注意:
-
A 行中的接口成员是调用签名。它看起来类似于方法签名,但没有名称。
-
结果的类型由冒号(而不是箭头)分隔,并且不能被省略。
一方面,接口更冗长。另一方面,它们让我们指定函数的属性(这很少见,但确实会发生):
interface Incrementor1 { (x: number): number; increment: number; }
我们还可以通过函数签名类型和对象字面类型的交集类型(
type Incrementor2 = (x: number) => number & { increment: number } ;
20.2.3?检查可调用值是否与函数类型匹配
例如,考虑以下情景:一个库导出以下函数类型。
type StringPredicate = (str: string) => boolean;
我们想要定义一个类型与
20.2.3.1?检查箭头函数
如果我们通过
const pred1: StringPredicate = (str) => str.length > 0;
注意,我们不需要指定参数
20.2.3.2?检查函数声明(简单)
检查函数声明更加复杂:
function pred2(str: string): boolean { return str.length > 0; } // Assign the function to a type-annotated variable const pred2ImplementsStringPredicate: StringPredicate = pred2;
20.2.3.3?检查函数声明(奢侈的)
以下解决方案有点过头(即,如果你不完全理解也不要担心),但它演示了几个高级特性:
function pred3(...[str]: Parameters<StringPredicate>) : ReturnType<StringPredicate> { return str.length > 0; }
-
参数:我们使用
Parameters<> 来提取具有参数类型的元组。三个点声明了一个剩余参数,它收集元组/数组中的所有参数。[str] 对该元组进行解构。(本章后面将更多介绍剩余参数。) -
返回值:我们使用
ReturnType<> 来提取返回类型。
20.3?参数
20.3.1?何时必须对参数进行类型注释?
回顾:如果打开了
在以下示例中,TypeScript 无法推断
function twice(str: string) { return str + str; }
在 A 行,TypeScript 可以使用类型
type StringMapFunction = (str: string) => string; const twice: StringMapFunction = (str) => str + str; // (A)
在这里,TypeScript 可以使用
assert.deepEqual( ['a', 'b', 'c'].map((str) => str + str), ['aa', 'bb', 'cc']);
这是
interface Array<T> { map<U>( callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any ): U[]; // ··· }
20.3.2?可选参数
在本节中,我们将看几种允许参数被省略的方法。
20.3.2.1?可选参数:str?: string
如果在参数名称后面加上问号,该参数就变成了可选的,在调用函数时可以省略:
function trim1(str?: string): string { // Internal type of str: // %inferred-type: string | undefined str; if (str === undefined) { return ''; } return str.trim(); } // External type of trim1: // %inferred-type: (str?: string | undefined) => string trim1;
这是
assert.equal( trim1(' abc '), 'abc'); assert.equal( trim1(), ''); // `undefined` is equivalent to omitting the parameter assert.equal( trim1(undefined), '');
20.3.2.2?联合类型:str: undefined|string
在外部,
function trim2(str: undefined|string): string { // Internal type of str: // %inferred-type: string | undefined str; if (str === undefined) { return ''; } return str.trim(); } // External type of trim2: // %inferred-type: (str: string | undefined) => string trim2;
assert.equal( trim2(' abc '), 'abc'); // @ts-expect-error: Expected 1 arguments, but got 0. (2554) trim2(); // (A) assert.equal( trim2(undefined), ''); // OK!
20.3.2.3?参数默认值:str = ''
如果我们为
function trim3(str = ''): string { // Internal type of str: // %inferred-type: string str; return str.trim(); } // External type of trim2: // %inferred-type: (str?: string) => string trim3;
注意,
让我们调用
assert.equal( trim3(' abc '), 'abc'); // Omitting the parameter triggers the parameter default value: assert.equal( trim3(), ''); // `undefined` is allowed and triggers the parameter default value: assert.equal( trim3(undefined), '');
20.3.2.4?参数默认值加类型注释
我们还可以同时指定类型和默认值:
function trim4(str: string = ''): string { return str.trim(); }
20.3.3?剩余参数
20.3.3.1?具有数组类型的剩余参数
剩余参数将所有剩余参数收集到一个数组中。因此,它的静态类型通常是数组。在下面的例子中,
function join(separator: string, ...parts: string[]) { return parts.join(separator); } assert.equal( join('-', 'state', 'of', 'the', 'art'), 'state-of-the-art');
20.3.3.2?具有元组类型的剩余参数
下一个示例演示了两个特性:
-
我们可以使用元组类型,如
[string, number] ,来作为剩余参数。 -
我们可以解构剩余参数(不仅仅是普通参数)。
function repeat1(...[str, times]: [string, number]): string { return str.repeat(times); }
function repeat2(str: string, times: number): string { return str.repeat(times); }
20.3.4?命名参数
命名参数是 JavaScript 中的一种流行模式,其中使用对象文字为每个参数指定名称。看起来如下:
assert.equal( padStart({str: '7', len: 3, fillStr: '0'}), '007');
在纯 JavaScript 中,函数可以使用解构来访问命名参数值。遗憾的是,在 TypeScript 中,我们还必须为对象文字指定类型,这导致了冗余:
function padStart({ str, len, fillStr = ' ' } // (A) : { str: string, len: number, fillStr: string }) { // (B) return str.padStart(len, fillStr); }
请注意,解构(包括
可以定义一个单独的类型,而不是我们在 B 行中使用的内联对象文字类型。但是,在大多数情况下,我更喜欢不这样做,因为它略微违反了参数的本质,参数是每个函数的本地和唯一的。如果您更喜欢在函数头中有更少的内容,那也可以。
20.3.5?this 作为参数(高级)
每个普通函数始终具有隐式参数
例如,考虑以下用于 DOM 事件源的接口(稍微简化版本):
interface EventSource { addEventListener( type: string, listener: (this: EventSource, ev: Event) => any, options?: boolean | AddEventListenerOptions ): void; // ··· }
回调
下一个示例演示了 TypeScript 如何使用
function toIsoString(this: Date): string { return this.toISOString(); } // @ts-expect-error: Argument of type '"abc"' is not assignable to // parameter of type 'Date'. (2345) assert.throws(() => toIsoString.call('abc')); // (A) error toIsoString.call(new Date()); // (B) OK
此外,我们不能将
const obj = { toIsoString }; // @ts-expect-error: The 'this' context of type // '{ toIsoString: (this: Date) => string; }' is not assignable to // method's 'this' of type 'Date'. [...] assert.throws(() => obj.toIsoString()); // error obj.toIsoString.call(new Date()); // OK
20.4?重载(高级)
有时单个类型签名无法充分描述函数的工作原理。
20.4.1?重载函数声明
考虑我们在以下示例中调用的
interface Customer { id: string; fullName: string; } const jane = {id: '1234', fullName: 'Jane Bond'}; const lars = {id: '5678', fullName: 'Lars Croft'}; const idToCustomer = new Map<string, Customer>([ ['1234', jane], ['5678', lars], ]); assert.equal( getFullName(idToCustomer, '1234'), 'Jane Bond'); // (A) assert.equal( getFullName(lars), 'Lars Croft'); // (B)
我们如何实现
function getFullName( customerOrMap: Customer | Map<string, Customer>, id?: string ): string { if (customerOrMap instanceof Map) { if (id === undefined) throw new Error(); const customer = customerOrMap.get(id); if (customer === undefined) { throw new Error('Unknown ID: ' + id); } customerOrMap = customer; } else { if (id !== undefined) throw new Error(); } return customerOrMap.fullName; }
但是,使用这种类型签名,编译时可以产生运行时错误的函数调用是合法的:
assert.throws(() => getFullName(idToCustomer)); // missing ID assert.throws(() => getFullName(lars, '5678')); // ID not allowed
以下代码修复了这些问题:
function getFullName(customerOrMap: Customer): string; // (A) function getFullName( // (B) customerOrMap: Map<string, Customer>, id: string): string; function getFullName( // (C) customerOrMap: Customer | Map<string, Customer>, id?: string ): string { // ··· } // @ts-expect-error: Argument of type 'Map<string, Customer>' is not // assignable to parameter of type 'Customer'. [...] getFullName(idToCustomer); // missing ID // @ts-expect-error: Argument of type '{ id: string; fullName: string; }' // is not assignable to parameter of type 'Map<string, Customer>'. // [...] getFullName(lars, '5678'); // ID not allowed
这里发生了什么?
-
实际实现从 C 行开始。与前面的示例相同。
-
在 A 行和 B 行中,有两个类型签名(没有主体的函数头),可以用于
getFullName() 。实际实现的类型签名不能使用!
我的建议是只有在无法避免时才使用重载。一种替代方法是将重载的函数拆分为具有不同名称的多个函数-例如:
-
getFullName() -
getFullNameViaMap()
20.4.2?通过接口进行重载
在接口中,我们可以有多个不同的调用签名。这使我们能够在以下示例中使用接口
interface GetFullName { (customerOrMap: Customer): string; (customerOrMap: Map<string, Customer>, id: string): string; } const getFullName: GetFullName = ( customerOrMap: Customer | Map<string, Customer>, id?: string ): string => { if (customerOrMap instanceof Map) { if (id === undefined) throw new Error(); const customer = customerOrMap.get(id); if (customer === undefined) { throw new Error('Unknown ID: ' + id); } customerOrMap = customer; } else { if (id !== undefined) throw new Error(); } return customerOrMap.fullName; }
20.4.3?基于字符串参数的重载(事件处理等)
在下一个示例中,我们通过接口进行重载并使用字符串文字类型(例如
function addEventListener(elem: HTMLElement, type: 'click', listener: (event: MouseEvent) => void): void; function addEventListener(elem: HTMLElement, type: 'keypress', listener: (event: KeyboardEvent) => void): void; function addEventListener(elem: HTMLElement, type: string, // (A) listener: (event: any) => void): void { elem.addEventListener(type, listener); // (B) }
在这种情况下,相对难以正确获取实现的类型(从 A 行开始)以使主体中的语句(B 行)起作用。作为最后的手段,我们总是可以使用类型
20.4.4?重载方法
20.4.4.1?重载具体方法
下一个示例演示了方法的重载:方法
class StringBuilder { #data = ''; add(num: number): this; add(bool: boolean): this; add(str: string): this; add(value: any): this { this.#data += String(value); return this; } toString() { return this.#data; } } const sb = new StringBuilder(); sb .add('I can see ') .add(3) .add(' monkeys!') ; assert.equal( sb.toString(), 'I can see 3 monkeys!')
20.4.4.2?重载接口方法
interface ArrayConstructor { from<T>(arrayLike: ArrayLike<T>): T[]; from<T, U>( arrayLike: ArrayLike<T>, mapfn: (v: T, k: number) => U, thisArg?: any ): U[]; }
-
在第一个签名中,返回的数组具有与参数相同的元素类型。
-
在第二个签名中,返回的数组元素与
mapfn 的结果具有相同的类型。这个版本的Array.from() 类似于Array.prototype.map() 。
20.5?可分配性(高级)
在本节中,我们将研究可分配性的类型兼容性规则:类型为
理解可分配性有助于我们回答诸如:
-
在函数调用中,对于形式参数的函数类型签名,哪些函数可以作为实际参数传递?
-
对于属性的函数类型签名,可以分配给它的函数是哪些?
20.5.1?可分配性的规则
在本小节中,我们将研究可分配性的一般规则(包括函数的规则)。在下一小节中,我们将探讨这些规则对函数的含义。
如果以下条件之一成立,则类型
-
Src 和Trg 是相同的类型。 -
Src 或Trg 是any 类型。 -
Src 是一个字符串字面类型,Trg 是原始类型 String。 -
Src 是一个联合类型,Src 的每个组成类型都可以分配给Trg 。 -
Src 和Trg 是函数类型,并且:-
Trg 具有剩余参数,或者Src 的必需参数数量小于或等于Trg 的总参数数量。 -
对于两个签名中都存在的参数,
Trg 中的每个参数类型都可以分配给Src 中对应的参数类型。 -
Trg 的返回类型是void ,或者Src 的返回类型可以分配给Trg 的返回类型。
-
-
(其余条件被省略。)
20.5.2?函数分配规则的后果
在本小节中,我们将研究分配规则对以下两个函数
const targetFunc: Trg = sourceFunc;
20.5.2.1?参数和结果的类型
-
目标参数类型必须可以分配给相应的源参数类型。
- 为什么?目标接受的任何内容也必须被源接受。
-
源返回类型必须可以分配给目标返回类型。
- 为什么?源返回的任何内容都必须与目标设置的期望兼容。
示例:
const trg1: (x: RegExp) => Object = (x: Object) => /abc/;
以下示例演示了如果目标返回类型是
const trg2: () => void = () => new Date();
20.5.2.2?参数的数量
源不得比目标具有更多的参数:
// @ts-expect-error: Type '(x: string) => string' is not assignable to // type '() => string'. (2322) const trg3: () => string = (x: string) => 'abc';
源可以比目标具有更少的参数:
const trg4: (x: string) => string = () => 'abc';
为什么?目标指定了对源的期望:它必须接受参数
['a', 'b'].map(x => x + x)
map<U>( callback: (value: T, index: number, array: T[]) => U, thisArg?: any ): U[];
20.6?本章的进一步阅读和来源
-
TypeScript 手册
-
TypeScript 语言规范
-
章节“可调用值” in “JavaScript for impatient programmers”
评论
第五部分:处理模糊类型
原文:
exploringjs.com/tackling-ts/pt_ambiguous-types.html 译者:飞龙
协议:CC BY-NC-SA 4.0
下一步:21 类型断言(与转换相关)
二十一、类型断言(与转换相关)
原文:
exploringjs.com/tackling-ts/ch_type-assertions.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
21.1?类型断言
-
21.1.1?类型断言的替代语法
-
21.1.2?示例:断言一个接口
-
21.1.3?示例:断言索引签名
-
-
21.2?与类型断言相关的构造
-
21.2.1?非空断言操作符(后缀
! ) -
21.2.2?明确赋值断言
-
本章讨论了 TypeScript 中的类型断言,它与其他语言中的类型转换相关,并通过
21.1?类型断言
类型断言允许我们覆盖 TypeScript 为值计算的静态类型。这对于解决类型系统的限制非常有用。
类型断言与其他语言中的类型转换相关,但它们不会抛出异常,也不会在运行时执行任何操作(它们在静态上执行了一些最小的检查)。
const data: object = ['a', 'b', 'c']; // (A) // @ts-expect-error: Property 'length' does not exist on type 'object'. data.length; // (B) assert.equal( (data as Array<string>).length, 3); // (C)
注释:
-
在 A 行,我们将数组的类型扩展为
object 。 -
在 B 行,我们看到这种类型不允许我们访问任何属性(详情)。
-
在 C 行,我们使用类型断言(操作符
as )告诉 TypeScriptdata 是一个数组。现在我们可以访问属性.length 。
类型断言是最后的手段,应尽量避免使用。它们(暂时地)移除了静态类型系统通常给我们的安全网。
请注意,在 A 行,我们还覆盖了 TypeScript 的静态类型。但我们是通过类型注释来实现的。这种覆盖方式比类型断言要安全得多,因为我们受到了更严格的约束:TypeScript 的类型必须可以赋值给注释的类型。
21.1.1?类型断言的替代语法
TypeScript 有一种替代的“尖括号”语法用于类型断言:
<Array<string>>data
我建议避免使用这种语法。它已经过时,并且与 React JSX 代码(在
21.1.2?示例:断言一个接口
为了访问任意对象
interface Named { name: string; } function getName(obj: object): string { if (typeof (obj as Named).name === 'string') { // (A) return (obj as Named).name; // (B) } return '(Unnamed)'; }
21.1.3?示例:断言索引签名
在以下代码(A 行)中,我们使用类型断言
type Dict = {[k:string]: any}; function getPropertyValue(dict: unknown, key: string): any { if (typeof dict === 'object' && dict !== null && key in dict) { // %inferred-type: object dict; // @ts-expect-error: Element implicitly has an 'any' type because // expression of type 'string' can't be used to index type '{}'. // [...] dict[key]; return (dict as Dict)[key]; // (A) } else { throw new Error(); } }
21.2?与类型断言相关的构造
21.2.1?非空断言操作符(后缀! )
如果值的类型是包括
const theName = 'Jane' as (null | string); // @ts-expect-error: Object is possibly 'null'. theName.length; assert.equal( theName!.length, 4); // OK
21.2.1.1?示例 - Map:.has() 后的.get()
在使用 Map 方法
function getLength(strMap: Map<string, string>, key: string): number { if (strMap.has(key)) { // We are sure x is not undefined: const value = strMap.get(key)!; // (A) return value.length; } return -1; }
我们可以在 Map 的值不能为
function getLength(strMap: Map<string, string>, key: string): number { // %inferred-type: string | undefined const value = strMap.get(key); if (value === undefined) { // (A) return -1; } // %inferred-type: string value; return value.length; }
21.2.2?明确赋值断言
如果打开了strict property initialization,我们偶尔需要告诉 TypeScript 我们确实初始化了某些属性 - 尽管它认为我们没有。
这是一个例子,即使不应该,TypeScript 也会抱怨:
class Point1 { // @ts-expect-error: Property 'x' has no initializer and is not definitely // assigned in the constructor. x: number; // @ts-expect-error: Property 'y' has no initializer and is not definitely // assigned in the constructor. y: number; constructor() { this.initProperties(); } initProperties() { this.x = 0; this.y = 0; } }
如果我们在 A 行和 B 行使用definite assignment assertions(感叹号),错误就会消失:
class Point2 { x!: number; // (A) y!: number; // (B) constructor() { this.initProperties(); } initProperties() { this.x = 0; this.y = 0; } }
评论
二十二、类型守卫和断言函数
原文:
exploringjs.com/tackling-ts/ch_type-guards-assertion-functions.html 译者:飞龙
协议:CC BY-NC-SA 4.0
-
22.1 静态类型何时过于泛化?
-
22.1.1 通过
if 和类型守卫缩小范围 -
22.1.2 通过
switch 和类型守卫缩小范围 -
22.1.3 类型过于泛化的更多情况
-
22.1.4 类型
unknown
-
-
22.2 通过内置类型守卫缩小范围
-
22.2.1 严格相等(
=== ) -
22.2.2
typeof 、instanceof 、Array.isArray -
22.2.3 通过
in 运算符检查不同的属性 -
22.2.4 检查共享属性的值(辨别联合)
-
22.2.5 缩小点名
-
22.2.6 缩小数组元素类型
-
-
22.3 用户定义的类型守卫
-
22.3.1 用户定义类型守卫的示例:
isArrayWithInstancesOf() -
22.3.2 用户定义类型守卫的示例:
isTypeof()
-
-
22.4 断言函数
-
22.4.1 TypeScript 对断言函数的支持
-
22.4.2 断言布尔类型的参数:
asserts ?cond? -
22.4.3 断言参数的类型:
asserts ?arg? is ?type?
-
-
22.5 快速参考:用户定义的类型守卫和断言函数
-
22.5.1 用户定义类型守卫
-
22.5.2 断言函数
-
-
22.6 断言函数的替代方法
-
22.6.1 技巧:强制转换
-
22.6.2 技巧:抛出异常
-
-
22.7
@hqoss/guards :带有类型守卫的库
在 TypeScript 中,一个值可能对于某些操作来说类型过于泛化,例如,联合类型。本章回答以下问题:
-
类型的缩小是什么?
- 剧透:缩小意味着将存储位置(例如变量或属性)的静态类型
T 更改为T 的子集。例如,将类型null|string 缩小为类型string 通常很有用。
- 剧透:缩小意味着将存储位置(例如变量或属性)的静态类型
-
类型守卫和断言函数是什么,我们如何使用它们来缩小类型?
- 剧透:
typeof 和instanceof 是类型守卫。
- 剧透:
22.1 当静态类型过于一般化时?
要看看静态类型如何过于一般化,请考虑以下函数
assert.equal( getScore('*****'), 5); assert.equal( getScore(3), 3);
function getScore(value: number|string): number { // ··· }
在
22.1.1 通过 if 和类型守卫缩小
解决方案是通过
function getScore(value: number|string): number { if (typeof value === 'number') { // (A) // %inferred-type: number value; return value; } if (typeof value === 'string') { // (B) // %inferred-type: string value; return value.length; } throw new Error('Unsupported value: ' + value); }
在本章中,我们将类型解释为值的集合。(有关此解释和另一种解释的更多信息,请参见[content not included]。)
在从 A 行和 B 行开始的 then-blocks 中,由于我们执行的检查,
请注意,缩小不会改变
22.1.2 通过 switch 和类型守卫缩小
如果我们使用
function getScore(value: number|string): number { switch (typeof value) { case 'number': // %inferred-type: number value; return value; case 'string': // %inferred-type: string value; return value.length; default: throw new Error('Unsupported value: ' + value); } }
22.1.3 类型过于一般化的更多情况
这些是类型过于一般化的更多例子:
-
可空类型:
function func1(arg: null|string) {} function func2(arg: undefined|string) {}
-
辨别联合:
type Teacher = { kind: 'Teacher', teacherId: string }; type Student = { kind: 'Student', studentId: string }; type Attendee = Teacher | Student; function func3(attendee: Attendee) {}
-
可选参数的类型:
function func4(arg?: string) { // %inferred-type: string | undefined arg; }
请注意,这些类型都是联合类型!
22.1.4 类型 unknown
如果一个值具有类型
function parseStringLiteral(stringLiteral: string): string { const result: unknown = JSON.parse(stringLiteral); if (typeof result === 'string') { // (A) return result; } throw new Error('Not a string literal: ' + stringLiteral); }
换句话说:类型
22.2 通过内置类型守卫缩小
正如我们所见,类型守卫 是一种操作,根据其运行时是否满足某些条件,返回
22.2.1 严格相等 (=== )
严格相等作为一种类型守卫:
function func(value: unknown) { if (value === 'abc') { // %inferred-type: "abc" value; } }
对于一些联合类型,我们可以使用
interface Book { title: null | string; isbn: string; } function getTitle(book: Book) { if (book.title === null) { // %inferred-type: null book.title; return '(Untitled)'; } else { // %inferred-type: string book.title; return book.title; } }
使用
22.2.2 typeof , instanceof , Array.isArray
这些是三种常见的内置类型守卫:
function func(value: Function|Date|number[]) { if (typeof value === 'function') { // %inferred-type: Function value; } if (value instanceof Date) { // %inferred-type: Date value; } if (Array.isArray(value)) { // %inferred-type: number[] value; } }
注意在 then-blocks 中
22.2.3 通过操作符 in 检查不同的属性
如果用于检查不同的属性,操作符
type FirstOrSecond = | {first: string} | {second: string}; function func(firstOrSecond: FirstOrSecond) { if ('second' in firstOrSecond) { // %inferred-type: { second: string; } firstOrSecond; } }
请注意以下检查将不起作用:
function func(firstOrSecond: FirstOrSecond) { // @ts-expect-error: Property 'second' does not exist on // type 'FirstOrSecond'. [...] if (firstOrSecond.second !== undefined) { // ··· } }
在这种情况下的问题是,如果不缩小,我们无法访问类型为
22.2.3.1 操作符 in 不会缩小非联合类型
遗憾的是,
function func(obj: object) { if ('name' in obj) { // %inferred-type: object obj; // @ts-expect-error: Property 'name' does not exist on type 'object'. obj.name; } }
22.2.4 检查共享属性的值(辨别联合)
在辨别联合中,联合类型的组件具有一个或多个共同的属性,其值对于每个组件都是不同的。这些属性称为辨别者。
检查辨别者的值是一种类型守卫:
type Teacher = { kind: 'Teacher', teacherId: string }; type Student = { kind: 'Student', studentId: string }; type Attendee = Teacher | Student; function getId(attendee: Attendee) { switch (attendee.kind) { case 'Teacher': // %inferred-type: { kind: "Teacher"; teacherId: string; } attendee; return attendee.teacherId; case 'Student': // %inferred-type: { kind: "Student"; studentId: string; } attendee; return attendee.studentId; default: throw new Error(); } }
在前面的例子中,
function getId(attendee: Attendee) { if (attendee.kind === 'Teacher') { // %inferred-type: { kind: "Teacher"; teacherId: string; } attendee; return attendee.teacherId; } else if (attendee.kind === 'Student') { // %inferred-type: { kind: "Student"; studentId: string; } attendee; return attendee.studentId; } else { throw new Error(); } }
22.2.5 缩小点名
我们还可以缩小属性的类型(甚至是通过属性名称链访问的嵌套属性的类型):
type MyType = { prop?: number | string, }; function func(arg: MyType) { if (typeof arg.prop === 'string') { // %inferred-type: string arg.prop; // (A) [].forEach((x) => { // %inferred-type: string | number | undefined arg.prop; // (B) }); // %inferred-type: string arg.prop; arg = {}; // %inferred-type: string | number | undefined arg.prop; // (C) } }
让我们看看前面代码中的几个位置:
-
A 行:我们通过类型守卫缩小了
arg.prop 的类型。 -
B 行:回调可能会在很久以后执行(考虑异步代码),这就是为什么 TypeScript 在回调内部取消缩小。
-
C 行:前面的赋值也取消了缩小。
22.2.6 缩小数组元素类型
22.2.6.1 数组方法.every() 不会缩小
如果我们使用
const mixedValues: ReadonlyArray<undefined|null|number> = [1, undefined, 2, null]; if (mixedValues.every(isNotNullish)) { // %inferred-type: readonly (number | null | undefined)[] mixedValues; // (A) }
请注意
前面的代码使用了以下用户定义的类型守卫(稍后会详细介绍):
function isNotNullish<T>(value: T): value is NonNullable<T> { // (A) return value !== undefined && value !== null; }
22.2.6.2 数组方法.filter() 产生具有更窄类型的数组
// %inferred-type: (number | null | undefined)[] const mixedValues = [1, undefined, 2, null]; // %inferred-type: number[] const numbers = mixedValues.filter(isNotNullish); function isNotNullish<T>(value: T): value is NonNullable<T> { // (A) return value !== undefined && value !== null; }
遗憾的是,我们必须直接使用类型守卫函数-箭头函数与类型守卫是不够的:
// %inferred-type: (number | null | undefined)[] const stillMixed1 = mixedValues.filter( x => x !== undefined && x !== null); // %inferred-type: (number | null | undefined)[] const stillMixed2 = mixedValues.filter( x => typeof x === 'number');
22.3 用户定义类型守卫
TypeScript 允许我们定义自己的类型守卫-例如:
function isFunction(value: unknown): value is Function { return typeof value === 'function'; }
返回类型
// %inferred-type: (value: unknown) => value is Function isFunction;
用户定义的类型守卫必须始终返回布尔值。如果
function func(arg: unknown) { if (isFunction(arg)) { // %inferred-type: Function arg; // type is narrowed } }
请注意,TypeScript 不关心我们如何计算用户定义类型守卫的结果。这给了我们很大的自由度,关于我们使用的检查。例如,我们可以将
function isFunction(value: any): value is Function { try { value(); // (A) return true; } catch { return false; } }
遗憾的是,我们必须对参数
22.3.1 用户定义类型守卫的示例:isArrayWithInstancesOf()
/** * This type guard for Arrays works similarly to `Array.isArray()`, * but also checks if all Array elements are instances of `T`. * As a consequence, the type of `arr` is narrowed to `Array<T>` * if this function returns `true`. * * Warning: This type guard can make code unsafe – for example: * We could use another reference to `arr` to add an element whose * type is not `T`. Then `arr` doesn’t have the type `Array<T>` * anymore. */ function isArrayWithInstancesOf<T>( arr: any, Class: new (...args: any[])=>T) : arr is Array<T> { if (!Array.isArray(arr)) { return false; } if (!arr.every(elem => elem instanceof Class)) { return false; } // %inferred-type: any[] arr; // (A) return true; }
在 A 行,我们可以看到
const value: unknown = {}; if (isArrayWithInstancesOf(value, RegExp)) { // %inferred-type: RegExp[] value; }
22.3.2 用户定义类型守卫的示例:isTypeof()
22.3.2.1 第一次尝试
这是在 TypeScript 中实现
/** * An implementation of the `typeof` operator. */ function isTypeof<T>(value: unknown, prim: T): value is T { if (prim === null) { return value === null; } return value !== null && (typeof prim) === (typeof value); }
理想情况下,我们可以通过字符串指定
const value: unknown = {}; if (isTypeof(value, 123)) { // %inferred-type: number value; }
22.3.2.2 使用重载
更好的解决方案是使用重载(有几种情况被省略):
/** * A partial implementation of the `typeof` operator. */ function isTypeof(value: any, typeString: 'boolean'): value is boolean; function isTypeof(value: any, typeString: 'number'): value is number; function isTypeof(value: any, typeString: 'string'): value is string; function isTypeof(value: any, typeString: string): boolean { return typeof value === typeString; } const value: unknown = {}; if (isTypeof(value, 'boolean')) { // %inferred-type: boolean value; }
(这个方法是由Nick Fisher提出的。)
22.3.2.3 使用接口作为类型映射
另一种方法是使用接口作为从字符串到类型的映射(有几种情况被省略):
interface TypeMap { boolean: boolean; number: number; string: string; } /** * A partial implementation of the `typeof` operator. */ function isTypeof<T extends keyof TypeMap>(value: any, typeString: T) : value is TypeMap[T] { return typeof value === typeString; } const value: unknown = {}; if (isTypeof(value, 'string')) { // %inferred-type: string value; }
(这个方法是由Ran Lottem提出的。)
22.4 断言函数
断言函数检查其参数是否满足某些条件,如果不满足则抛出异常。例如,许多语言支持的一个断言函数是
在 Node.js 上,
import assert from 'assert'; function removeFilenameExtension(filename: string) { const dotIndex = filename.lastIndexOf('.'); assert(dotIndex >= 0); // (A) return filename.slice(0, dotIndex); }
22.4.1 TypeScript 对断言函数的支持
如果我们使用断言签名作为返回类型标记这样的函数,TypeScript 的类型推断将特别支持断言函数。就函数的返回方式和返回内容而言,断言签名等同于
有两种断言签名:
-
断言布尔参数:
asserts ?cond? -
断言参数的类型:
asserts ?arg? is ?type?
22.4.2 断言布尔参数:asserts ?cond?
在下面的例子中,断言签名
function assertTrue(condition: boolean): asserts condition { if (!condition) { throw new Error(); } }
这就是
function func(value: unknown) { assertTrue(value instanceof Set); // %inferred-type: Set<any> value; }
我们使用参数
22.4.3?断言参数的类型:asserts ?arg? is ?type?
在下面的例子中,断言签名
function assertIsNumber(value: any): asserts value is number { if (typeof value !== 'number') { throw new TypeError(); } }
这次,调用断言函数会缩小其参数的类型:
function func(value: unknown) { assertIsNumber(value); // %inferred-type: number value; }
22.4.3.1?示例断言函数:向对象添加属性
函数
function addXY<T>(obj: T, x: number, y: number) : asserts obj is (T & { x: number, y: number }) { // Adding properties via = would be more complicated... Object.assign(obj, {x, y}); } const obj = { color: 'green' }; addXY(obj, 9, 4); // %inferred-type: { color: string; } & { x: number; y: number; } obj;
交集类型
22.5?快速参考:用户定义的类型守卫和断言函数
22.5.1?用户定义的类型守卫
function isString(value: unknown): value is string { return typeof value === 'string'; }
-
类型谓词:
value is string -
结果:
boolean
22.5.2?断言函数
22.5.2.1?断言签名:asserts ?cond?
function assertTrue(condition: boolean): asserts condition { if (!condition) { throw new Error(); // assertion error } }
-
断言签名:
asserts condition -
结果:
void ,异常
22.5.2.2?断言签名:asserts ?arg? is ?type?
function assertIsString(value: unknown): asserts value is string { if (typeof value !== 'string') { throw new Error(); // assertion error } }
-
断言签名:
asserts value is string -
结果:
void ,异常
22.6?断言函数的替代方法
22.6.1?技术:强制转换
断言函数会缩小现有值的类型。强制转换函数会返回具有新类型的现有值 - 例如:
function forceNumber(value: unknown): number { if (typeof value !== 'number') { throw new TypeError(); } return value; } const value1a: unknown = 123; // %inferred-type: number const value1b = forceNumber(value1a); const value2: unknown = 'abc'; assert.throws(() => forceNumber(value2));
相应的断言函数如下所示:
function assertIsNumber(value: unknown): asserts value is number { if (typeof value !== 'number') { throw new TypeError(); } } const value1: unknown = 123; assertIsNumber(value1); // %inferred-type: number value1; const value2: unknown = 'abc'; assert.throws(() => assertIsNumber(value2));
强制转换是一种多用途的技术,除了断言函数的用途之外还有其他用途。例如,我们可以转换:
-
从易于编写的输入格式(比如 JSON 模式)
-
转换为易于在代码中使用的输出格式。
有关更多信息,请参见[content not included]。
22.6.2?技术:抛出异常
考虑以下代码:
function getLengthOfValue(strMap: Map<string, string>, key: string) : number { if (strMap.has(key)) { const value = strMap.get(key); // %inferred-type: string | undefined value; // before type check // We know that value can’t be `undefined` if (value === undefined) { // (A) throw new Error(); } // %inferred-type: string value; // after type check return value.length; } return -1; }
我们也可以使用断言函数来代替从 A 行开始的
assertNotUndefined(value);
如果我们不想编写这样的函数,抛出异常是一个快速的替代方法。与调用断言函数类似,这种技术也会更新静态类型。
22.7?@hqoss/guards :带有类型守卫的库
库
-
基本类型:
isBoolean() ,isNumber() ,等等。 -
特定类型:
isObject() ,isNull() ,isFunction() ,等等。 -
各种检查:
isNonEmptyArray() ,isInteger() ,等等。
评论