背景阅读
类 (MDN)
TypeScript 完全支持 ES2015 中引入的 class
关键字。
与其他 JavaScript 语言特性一样,TypeScript 添加了类型注解和其他语法,使您可以表达类与其他类型之间的关系。
类成员
这是最基本的类 - 一个空类
tsTry
classPoint {}
这个类目前还不太有用,所以让我们开始添加一些成员。
字段
字段声明在类上创建一个公共可写属性
tsTry
classPoint {x : number;y : number;}constpt = newPoint ();pt .x = 0;pt .y = 0;
与其他位置一样,类型注解是可选的,但如果没有指定,将是隐式的 any
。
字段也可以有初始化器;这些将在类实例化时自动运行
tsTry
classPoint {x = 0;y = 0;}constpt = newPoint ();// Prints 0, 0console .log (`${pt .x }, ${pt .y }`);
就像 const
、let
和 var
一样,类属性的初始化器将用于推断其类型
tsTry
constpt = newPoint ();Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.pt .x = "0";
--strictPropertyInitialization
该 strictPropertyInitialization
设置控制类字段是否需要在构造函数中初始化。
tsTry
classBadGreeter {Property 'name' has no initializer and is not definitely assigned in the constructor.2564Property 'name' has no initializer and is not definitely assigned in the constructor.: string; name }
tsTry
classGoodGreeter {name : string;constructor() {this.name = "hello";}}
请注意,字段需要在构造函数本身中初始化。TypeScript 不会分析您从构造函数调用的方法来检测初始化,因为派生类可能会覆盖这些方法并无法初始化成员。
如果您打算通过构造函数以外的方式(例如,某个外部库正在为您填充类的一部分)来明确初始化字段,则可以使用 *明确赋值断言运算符*,即 !
。
tsTry
classOKGreeter {// Not initialized, but no errorname !: string;}
readonly
字段可以以 readonly
修饰符为前缀。这将阻止在构造函数之外对字段进行赋值。
tsTry
classGreeter {readonlyname : string = "world";constructor(otherName ?: string) {if (otherName !==undefined ) {this.name =otherName ;}}err () {this.Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.= "not ok"; name }}constg = newGreeter ();Cannot assign to 'name' because it is a read-only property.2540Cannot assign to 'name' because it is a read-only property.g .= "also not ok"; name
构造函数
背景阅读
构造函数 (MDN)
类构造函数与函数非常相似。您可以添加带有类型注解、默认值和重载的参数。
tsTry
classPoint {x : number;y : number;// Normal signature with defaultsconstructor(x = 0,y = 0) {this.x =x ;this.y =y ;}}
tsTry
classPoint {// Overloadsconstructor(x : number,y : string);constructor(s : string);constructor(xs : any,y ?: any) {// TBD}}
类构造函数签名和函数签名之间只有几个区别。
- 构造函数不能有类型参数 - 这些属于外部类声明,我们将在后面学习。
- 构造函数不能有返回值类型注解 - 类实例类型始终是返回的内容。
超级调用
就像在 JavaScript 中一样,如果您有一个基类,您需要在构造函数体中调用 super();
,然后再使用任何 this.
成员。
tsTry
classBase {k = 4;}classDerived extendsBase {constructor() {// Prints a wrong value in ES5; throws exception in ES6'super' must be called before accessing 'this' in the constructor of a derived class.17009'super' must be called before accessing 'this' in the constructor of a derived class.console .log (this .k );super();}}
忘记调用 super
是在 JavaScript 中很容易犯的错误,但 TypeScript 会在必要时告诉您。
方法
背景阅读
方法定义
类上的函数属性称为方法。方法可以使用与函数和构造函数相同的类型注释。
tsTry
classPoint {x = 10;y = 10;scale (n : number): void {this.x *=n ;this.y *=n ;}}
除了标准类型注释外,TypeScript 不会在方法中添加任何其他新内容。
请注意,在方法体内部,仍然必须通过this.
访问字段和其他方法。方法体中的非限定名称始终引用封闭范围中的内容。
tsTry
letx : number = 0;classC {x : string = "hello";m () {// This is trying to modify 'x' from line 1, not the class propertyType 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.= "world"; x }}
Getter/Setter
类也可以有访问器
tsTry
classC {_length = 0;getlength () {return this._length ;}setlength (value ) {this._length =value ;}}
请注意,在 JavaScript 中,没有额外逻辑的字段支持的 get/set 对很少有用。如果您不需要在 get/set 操作期间添加其他逻辑,则公开公共字段是可以的。
TypeScript 对访问器有一些特殊的推断规则。
- 如果存在
get
但不存在set
,则该属性将自动变为readonly
。 - 如果 setter 参数的类型未指定,则会从 getter 的返回类型推断出来。
- Getter 和 setter 必须具有相同的成员可见性
从TypeScript 4.3开始,可以为获取和设置使用不同类型的访问器。
tsTry
classThing {_size = 0;getsize (): number {return this._size ;}setsize (value : string | number | boolean) {letnum =Number (value );// Don't allow NaN, Infinity, etcif (!Number .isFinite (num )) {this._size = 0;return;}this._size =num ;}}
索引签名
类可以声明索引签名;它们的工作方式与其他对象类型的索引签名相同。
tsTry
classMyClass {[s : string]: boolean | ((s : string) => boolean);check (s : string) {return this[s ] as boolean;}}
由于索引签名类型还需要捕获方法的类型,因此很难有效地使用这些类型。通常,最好将索引数据存储在其他地方,而不是在类实例本身。
类继承
与其他具有面向对象功能的语言一样,JavaScript 中的类可以从基类继承。
implements
子句
您可以使用 implements
子句来检查类是否满足特定的 interface
。如果类未能正确实现它,将发出错误。
tsTry
interfacePingable {ping (): void;}classSonar implementsPingable {ping () {console .log ("ping!");}}classClass 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.2420Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.implements Ball Pingable {pong () {console .log ("pong!");}}
类也可以实现多个接口,例如:class C implements A, B {
。
注意事项
重要的是要理解,implements
语句只是一种检查,确保类可以被视为接口类型。它完全不会改变类的类型或其方法。一个常见的错误来源是假设 implements
语句会改变类的类型 - 它不会!
tsTry
interfaceCheckable {check (name : string): boolean;}classNameChecker implementsCheckable {Parameter 's' implicitly has an 'any' type.7006Parameter 's' implicitly has an 'any' type.check () { s // Notice no error herereturns .toLowerCase () === "ok";}}
在这个例子中,我们可能期望 s
的类型会受到 check
的 name: string
参数的影响。但事实并非如此 - implements
语句不会改变类主体如何被检查或其类型如何被推断。
类似地,实现具有可选属性的接口不会创建该属性。
tsTry
interfaceA {x : number;y ?: number;}classC implementsA {x = 0;}constc = newC ();Property 'y' does not exist on type 'C'.2339Property 'y' does not exist on type 'C'.c .= 10; y
extends
语句
背景阅读
extends 关键字 (MDN)
类可以从基类extend
。派生类拥有其基类所有属性和方法,并且还可以定义额外的成员。
tsTry
classAnimal {move () {console .log ("Moving along!");}}classDog extendsAnimal {woof (times : number) {for (leti = 0;i <times ;i ++) {console .log ("woof!");}}}constd = newDog ();// Base class methodd .move ();// Derived class methodd .woof (3);
覆盖方法
背景阅读
super 关键字 (MDN)
派生类也可以覆盖基类字段或属性。可以使用 super.
语法访问基类方法。请注意,由于 JavaScript 类是简单的查找对象,因此没有“超级字段”的概念。
TypeScript 强制派生类始终是其基类的子类型。
例如,以下是一种覆盖方法的合法方式
tsTry
classBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {greet (name ?: string) {if (name ===undefined ) {super.greet ();} else {console .log (`Hello, ${name .toUpperCase ()}`);}}}constd = newDerived ();d .greet ();d .greet ("reader");
派生类必须遵循其基类的契约。请记住,通过基类引用引用派生类实例非常常见(并且始终合法!)
tsTry
// Alias the derived instance through a base class referenceconstb :Base =d ;// No problemb .greet ();
如果Derived
没有遵循Base
的契约会怎样?
tsTry
classBase {greet () {console .log ("Hello, world!");}}classDerived extendsBase {// Make this parameter requiredProperty 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.2416Property 'greet' in type 'Derived' is not assignable to the same property in base type 'Base'. Type '(name: string) => void' is not assignable to type '() => void'.( greet name : string) {console .log (`Hello, ${name .toUpperCase ()}`);}}
如果我们在出现错误的情况下编译了这段代码,这个示例就会崩溃
tsTry
constb :Base = newDerived ();// Crashes because "name" will be undefinedb .greet ();
仅类型字段声明
当target >= ES2022
或useDefineForClassFields
为true
时,类字段在父类构造函数完成之后初始化,覆盖父类设置的任何值。当您只想为继承的字段重新声明更准确的类型时,这可能是一个问题。为了处理这些情况,您可以编写declare
来指示 TypeScript 此字段声明不应产生任何运行时影响。
tsTry
interfaceAnimal {dateOfBirth : any;}interfaceDog extendsAnimal {breed : any;}classAnimalHouse {resident :Animal ;constructor(animal :Animal ) {this.resident =animal ;}}classDogHouse extendsAnimalHouse {// Does not emit JavaScript code,// only ensures the types are correctdeclareresident :Dog ;constructor(dog :Dog ) {super(dog );}}
初始化顺序
JavaScript 类初始化的顺序在某些情况下可能令人惊讶。让我们考虑以下代码
tsTry
classBase {name = "base";constructor() {console .log ("My name is " + this.name );}}classDerived extendsBase {name = "derived";}// Prints "base", not "derived"constd = newDerived ();
这里发生了什么?
JavaScript 定义的类初始化顺序为
- 初始化基类字段
- 运行基类构造函数
- 初始化派生类字段
- 运行派生类构造函数
这意味着基类构造函数在其自己的构造函数期间看到了它自己的name
值,因为派生类字段初始化尚未运行。
继承内置类型
注意:如果您不打算从内置类型(如
Array
、Error
、Map
等)继承,或者您的编译目标明确设置为ES6
/ES2015
或更高版本,则可以跳过本节。
在 ES2015 中,返回对象的构造函数会隐式地将 this
的值替换为 super(...)
的任何调用者的值。生成的构造函数代码需要捕获 super(...)
的任何潜在返回值,并将其替换为 this
。
因此,对 Error
、Array
等的子类化可能不再按预期工作。这是因为 Error
、Array
等的构造函数使用 ECMAScript 6 的 new.target
来调整原型链;但是,在 ECMAScript 5 中调用构造函数时,无法确保 new.target
的值。其他降级编译器默认情况下通常具有相同的限制。
对于以下子类
tsTry
classMsgError extendsError {constructor(m : string) {super(m );}sayHello () {return "hello " + this.message ;}}
您可能会发现
- 方法在通过构造这些子类返回的对象上可能是
undefined
,因此调用sayHello
将导致错误。 instanceof
将在子类的实例及其实例之间被破坏,因此(new MsgError()) instanceof MsgError
将返回false
。
建议您在任何 super(...)
调用之后立即手动调整原型。
tsTry
classMsgError extendsError {constructor(m : string) {super(m );// Set the prototype explicitly.Object .setPrototypeOf (this,MsgError .prototype );}sayHello () {return "hello " + this.message ;}}
但是,MsgError
的任何子类都必须手动设置原型。对于不支持 Object.setPrototypeOf
的运行时,您可能可以使用 __proto__
。
不幸的是,这些解决方法在 Internet Explorer 10 及更早版本上无效。可以手动将方法从原型复制到实例本身(例如,将 MsgError.prototype
复制到 this
),但原型链本身无法修复。
成员可见性
可以使用 TypeScript 来控制某些方法或属性是否对类外部代码可见。
public
类成员的默认可见性为 public
。public
成员可以在任何地方访问。
tsTry
classGreeter {publicgreet () {console .log ("hi!");}}constg = newGreeter ();g .greet ();
由于 public
已经是默认的可见性修饰符,因此不需要在类成员上编写它,但可以选择这样做以提高样式/可读性。
protected
protected
成员仅对声明它们的类的子类可见。
tsTry
classGreeter {publicgreet () {console .log ("Hello, " + this.getName ());}protectedgetName () {return "hi";}}classSpecialGreeter extendsGreeter {publichowdy () {// OK to access protected member hereconsole .log ("Howdy, " + this.getName ());}}constg = newSpecialGreeter ();g .greet (); // OKProperty 'getName' is protected and only accessible within class 'Greeter' and its subclasses.2445Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.g .(); getName
protected
成员的暴露
派生类需要遵循其基类的契约,但可以选择公开具有更多功能的基类子类型。这包括将protected
成员设为public
tsTry
classBase {protectedm = 10;}classDerived extendsBase {// No modifier, so default is 'public'm = 15;}constd = newDerived ();console .log (d .m ); // OK
请注意,Derived
已经能够自由地读取和写入m
,因此这不会对这种情况的“安全性”产生实质性影响。这里需要注意的主要问题是,在派生类中,如果这种公开不是故意的,我们需要小心地重复protected
修饰符。
跨层次结构protected
访问
不同的面向对象编程语言在是否允许通过基类引用访问protected
成员方面存在分歧
tsTry
classBase {protectedx : number = 1;}classDerived1 extendsBase {protectedx : number = 5;}classDerived2 extendsBase {f1 (other :Derived2 ) {other .x = 10;}f2 (other :Derived1 ) {Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.2445Property 'x' is protected and only accessible within class 'Derived1' and its subclasses.other .= 10; x }}
例如,Java认为这是合法的。另一方面,C#和C++选择将此代码视为非法。
TypeScript站在C#和C++一边,因为在Derived2
中访问x
应该只允许从Derived2
的子类进行,而Derived1
不是其中之一。此外,如果通过Derived1
引用访问x
是非法的(这当然应该是!),那么通过基类引用访问它不应该改善这种情况。
另请参阅为什么我不能从派生类访问受保护的成员?,其中解释了更多关于C#的推理。
private
private
类似于protected
,但即使从子类也不能访问该成员
tsTry
classBase {privatex = 0;}constb = newBase ();// Can't access from outside the classProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (b .); x
tsTry
classDerived extendsBase {showX () {// Can't access in subclassesProperty 'x' is private and only accessible within class 'Base'.2341Property 'x' is private and only accessible within class 'Base'.console .log (this.); x }}
由于派生类无法看到private
成员,因此派生类无法提高其可见性
tsTry
classBase {privatex = 0;}classClass 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.2415Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'.extends Derived Base {x = 1;}
跨实例 `private` 访问
不同的面向对象编程语言在同一个类的不同实例是否可以访问彼此的 `private` 成员方面存在分歧。虽然像 Java、C#、C++、Swift 和 PHP 这样的语言允许这样做,但 Ruby 不允许。
TypeScript 允许跨实例 `private` 访问。
tsTry
classA {privatex = 10;publicsameAs (other :A ) {// No errorreturnother .x === this.x ;}}
注意事项
与 TypeScript 类型系统中的其他方面一样,`private` 和 `protected` 仅在类型检查期间强制执行。
这意味着 JavaScript 运行时构造,如 `in` 或简单的属性查找,仍然可以访问 `private` 或 `protected` 成员。
tsTry
classMySafe {privatesecretKey = 12345;}
js
// In a JavaScript file...const s = new MySafe();// Will print 12345console.log(s.secretKey);
`private` 也允许在类型检查期间使用方括号表示法访问。这使得 `private` 声明的字段在进行单元测试时可能更容易访问,但缺点是这些字段是“软私有”的,并没有严格地强制执行隐私。
tsTry
classMySafe {privatesecretKey = 12345;}consts = newMySafe ();// Not allowed during type checkingProperty 'secretKey' is private and only accessible within class 'MySafe'.2341Property 'secretKey' is private and only accessible within class 'MySafe'.console .log (s .); secretKey // OKconsole .log (s ["secretKey"]);
与 TypeScript 的 `private` 不同,JavaScript 的 私有字段 (#
) 在编译后仍然是私有的,并且不提供前面提到的像方括号表示法访问这样的逃逸途径,使它们成为“硬私有”。
tsTry
classDog {#barkAmount = 0;personality = "happy";constructor() {}}
tsTry
"use strict";class Dog {#barkAmount = 0;personality = "happy";constructor() { }}
当编译到 ES2021 或更低版本时,TypeScript 将使用 WeakMaps 代替 #
。
tsTry
"use strict";var _Dog_barkAmount;class Dog {constructor() {_Dog_barkAmount.set(this, 0);this.personality = "happy";}}_Dog_barkAmount = new WeakMap();
如果您需要保护类中的值免受恶意行为者的攻击,您应该使用提供严格运行时隐私的机制,例如闭包、WeakMaps 或私有字段。请注意,这些在运行时添加的隐私检查可能会影响性能。
静态成员
背景阅读
静态成员 (MDN)
类可以拥有 static
成员。这些成员不与类的特定实例相关联。可以通过类构造函数对象本身访问它们。
tsTry
classMyClass {staticx = 0;staticprintX () {console .log (MyClass .x );}}console .log (MyClass .x );MyClass .printX ();
静态成员也可以使用相同的 public
、protected
和 private
可见性修饰符。
tsTry
classMyClass {private staticx = 0;}Property 'x' is private and only accessible within class 'MyClass'.2341Property 'x' is private and only accessible within class 'MyClass'.console .log (MyClass .); x
静态成员也是继承的。
tsTry
classBase {staticgetGreeting () {return "Hello world";}}classDerived extendsBase {myGreeting =Derived .getGreeting ();}
特殊静态名称
一般来说,覆盖 Function
原型中的属性是不安全或不可能的。因为类本身是可以用 new
调用的函数,所以某些 static
名称不能使用。像 name
、length
和 call
这样的函数属性不能定义为 static
成员。
tsTry
classS {staticStatic property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.2699Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'.= "S!"; name }
为什么没有静态类?
TypeScript(和 JavaScript)没有像 C# 那样叫做static class
的结构。
这些结构只存在是因为这些语言强制所有数据和函数都必须在类中;因为 TypeScript 没有这种限制,所以不需要它们。只有一个实例的类通常在 JavaScript/TypeScript 中用普通的对象表示。
例如,我们不需要在 TypeScript 中使用“静态类”语法,因为普通的对象(甚至顶层函数)也能很好地完成工作。
tsTry
// Unnecessary "static" classclassMyStaticClass {staticdoSomething () {}}// Preferred (alternative 1)functiondoSomething () {}// Preferred (alternative 2)constMyHelperObject = {dosomething () {},};
类中的static
块
静态块允许你编写一系列具有自身作用域的语句,这些语句可以访问包含类的私有字段。这意味着我们可以编写初始化代码,它拥有编写语句的所有功能,没有变量泄漏,并且可以完全访问我们类的内部。
tsTry
classFoo {static #count = 0;getcount () {returnFoo .#count;}static {try {constlastInstances =loadLastInstances ();Foo .#count +=lastInstances .length ;}catch {}}}
泛型类
类,就像接口一样,可以是泛型的。当使用 `new` 实例化泛型类时,其类型参数的推断方式与函数调用中的方式相同。
tsTry
classBox <Type > {contents :Type ;constructor(value :Type ) {this.contents =value ;}}constb = newBox ("hello!");
类可以使用泛型约束和默认值,与接口的方式相同。
静态成员中的类型参数
这段代码是非法的,可能并不明显为什么。
tsTry
classBox <Type > {staticStatic members cannot reference class type parameters.2302Static members cannot reference class type parameters.defaultValue :; Type }
请记住,类型总是会被完全擦除!在运行时,只有一个 `Box.defaultValue` 属性槽。这意味着设置 `Box<string>.defaultValue`(如果可能的话)也会改变 `Box<number>.defaultValue` - 这不好。泛型类的 `static` 成员永远不能引用类的类型参数。
this
在类中的运行时
背景阅读
this 关键字 (MDN)
重要的是要记住,TypeScript 不会改变 JavaScript 的运行时行为,而 JavaScript 因其一些奇特的运行时行为而闻名。
JavaScript 对 `this` 的处理确实不寻常。
tsTry
classMyClass {name = "MyClass";getName () {return this.name ;}}constc = newMyClass ();constobj = {name : "obj",getName :c .getName ,};// Prints "obj", not "MyClass"console .log (obj .getName ());
简而言之,默认情况下,函数内部 `this` 的值取决于函数的调用方式。在这个例子中,由于函数是通过 `obj` 引用调用的,因此它的 `this` 值是 `obj` 而不是类实例。
这很少是你想要发生的事情!TypeScript 提供了一些方法来减轻或防止这种错误。
箭头函数
背景阅读
箭头函数 (MDN)
如果你有一个函数,它经常以丢失其 this
上下文的方式被调用,那么使用箭头函数属性而不是方法定义可能是有意义的
tsTry
classMyClass {name = "MyClass";getName = () => {return this.name ;};}constc = newMyClass ();constg =c .getName ;// Prints "MyClass" instead of crashingconsole .log (g ());
这有一些权衡
this
值在运行时保证是正确的,即使对于没有用 TypeScript 检查的代码也是如此- 这将使用更多内存,因为每个类实例将拥有自己定义的每个函数的副本
- 你不能在派生类中使用
super.getName
,因为原型链中没有条目可以从中获取基类方法
this
参数
在方法或函数定义中,名为 this
的初始参数在 TypeScript 中具有特殊含义。这些参数在编译期间被擦除
tsTry
// TypeScript input with 'this' parameterfunctionfn (this :SomeType ,x : number) {/* ... */}
js
// JavaScript outputfunction fn(x) {/* ... */}
TypeScript 检查使用 this
参数调用函数是否以正确的上下文完成。我们可以向方法定义添加 this
参数,而不是使用箭头函数,以静态地强制方法被正确调用
tsTry
classMyClass {name = "MyClass";getName (this :MyClass ) {return this.name ;}}constc = newMyClass ();// OKc .getName ();// Error, would crashconstg =c .getName ;The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.2684The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.console .log (g ());
此方法与箭头函数方法的权衡相反
- JavaScript 调用者可能仍然会错误地使用类方法,而没有意识到这一点
- 每个类定义只分配一个函数,而不是每个类实例分配一个函数
- 基方法定义仍然可以通过
super
调用。
this
类型
在类中,一个名为 this
的特殊类型 *动态地* 指向当前类的类型。让我们看看这如何有用。
tsTry
classBox {contents : string = "";set (value : string) {this.contents =value ;return this;}}
这里,TypeScript 推断出 set
的返回值类型为 this
,而不是 Box
。现在让我们创建一个 Box
的子类。
tsTry
classClearableBox extendsBox {clear () {this.contents = "";}}consta = newClearableBox ();constb =a .set ("hello");
你也可以在参数类型注解中使用 this
。
tsTry
classBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}
这与写 other: Box
不同——如果你有一个派生类,它的 sameAs
方法现在将只接受该相同派生类的其他实例。
tsTry
classBox {content : string = "";sameAs (other : this) {returnother .content === this.content ;}}classDerivedBox extendsBox {otherContent : string = "?";}constbase = newBox ();constderived = newDerivedBox ();Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.2345Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.derived .sameAs (); base
基于 this
的类型守卫
你可以在类和接口中的方法的返回值位置使用 this is Type
。当与类型收窄(例如 if
语句)混合使用时,目标对象的类型将被收窄为指定的 Type
。
tsTry
classFileSystemObject {isFile (): this isFileRep {return this instanceofFileRep ;}isDirectory (): this isDirectory {return this instanceofDirectory ;}isNetworked (): this isNetworked & this {return this.networked ;}constructor(publicpath : string, privatenetworked : boolean) {}}classFileRep extendsFileSystemObject {constructor(path : string, publiccontent : string) {super(path , false);}}classDirectory extendsFileSystemObject {children :FileSystemObject [];}interfaceNetworked {host : string;}constfso :FileSystemObject = newFileRep ("foo/bar.txt", "foo");if (fso .isFile ()) {fso .content ;} else if (fso .isDirectory ()) {fso .children ;} else if (fso .isNetworked ()) {fso .host ;}
基于 this
的类型守卫的一个常见用例是允许对特定字段进行延迟验证。例如,这种情况下,当 hasValue
被验证为 true 时,会从 box 中保存的值中移除一个 undefined
。
tsTry
classBox <T > {value ?:T ;hasValue (): this is {value :T } {return this.value !==undefined ;}}constbox = newBox <string>();box .value = "Gameboy";box .value ;if (box .hasValue ()) {box .value ;}
参数属性
TypeScript 提供了一种特殊的语法,用于将构造函数参数转换为具有相同名称和值的类属性。这些被称为 *参数属性*,通过在构造函数参数前添加可见性修饰符 public
、private
、protected
或 readonly
来创建。生成的字段将获得这些修饰符。
tsTry
classParams {constructor(public readonlyx : number,protectedy : number,privatez : number) {// No body necessary}}consta = newParams (1, 2, 3);console .log (a .x );Property 'z' is private and only accessible within class 'Params'.2341Property 'z' is private and only accessible within class 'Params'.console .log (a .); z
类表达式
背景阅读
类表达式 (MDN)
类表达式与类声明非常相似。唯一的真正区别是类表达式不需要名称,尽管我们可以通过它们最终绑定的任何标识符来引用它们。
tsTry
constsomeClass = class<Type > {content :Type ;constructor(value :Type ) {this.content =value ;}};constm = newsomeClass ("Hello, world");
构造函数签名
JavaScript 类使用 new
运算符实例化。给定类本身的类型,InstanceType 实用类型对该操作进行建模。
tsTry
classPoint {createdAt : number;x : number;y : numberconstructor(x : number,y : number) {this.createdAt =Date .now ()this.x =x ;this.y =y ;}}typePointInstance =InstanceType <typeofPoint >functionmoveRight (point :PointInstance ) {point .x += 5;}constpoint = newPoint (3, 4);moveRight (point );point .x ; // => 8
abstract
类和成员
TypeScript 中的类、方法和字段可以是抽象的。
抽象方法或抽象字段是没有提供实现的方法或字段。这些成员必须存在于抽象类中,抽象类不能直接实例化。
抽象类的作用是作为子类的基类,子类实现所有抽象成员。当一个类没有任何抽象成员时,它被称为具体类。
让我们看一个例子
tsTry
abstract classBase {abstractgetName (): string;printName () {console .log ("Hello, " + this.getName ());}}constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.b = newBase ();
我们不能使用new
实例化Base
,因为它是一个抽象类。相反,我们需要创建一个派生类并实现抽象成员
tsTry
classDerived extendsBase {getName () {return "world";}}constd = newDerived ();d .printName ();
注意,如果我们忘记实现基类的抽象成员,我们会得到一个错误
tsTry
classNon-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.2515Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'.extends Derived Base {// forgot to do anything}
抽象构造签名
有时你希望接受一些类构造函数,它生成一个派生自某个抽象类的类的实例。
例如,你可能想编写以下代码
tsTry
functiongreet (ctor : typeofBase ) {constCannot create an instance of an abstract class.2511Cannot create an instance of an abstract class.instance = newctor ();instance .printName ();}
TypeScript 正确地告诉你,你正在尝试实例化一个抽象类。毕竟,根据greet
的定义,编写以下代码是完全合法的,这将最终构造一个抽象类
tsTry
// Bad!greet (Base );
相反,你希望编写一个接受具有构造签名的函数
tsTry
functiongreet (ctor : new () =>Base ) {constinstance = newctor ();instance .printName ();}greet (Derived );Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.2345Argument of type 'typeof Base' is not assignable to parameter of type 'new () => Base'. Cannot assign an abstract constructor type to a non-abstract constructor type.greet (); Base
现在 TypeScript 正确地告诉你哪些类构造函数可以被调用 - Derived
可以,因为它是一个具体类,但Base
不可以。
类之间的关系
在大多数情况下,TypeScript 中的类是结构化比较的,与其他类型相同。
例如,这两个类可以互换使用,因为它们是相同的。
tsTry
classPoint1 {x = 0;y = 0;}classPoint2 {x = 0;y = 0;}// OKconstp :Point1 = newPoint2 ();
类似地,类之间的子类型关系即使没有显式继承也存在。
tsTry
classPerson {name : string;age : number;}classEmployee {name : string;age : number;salary : number;}// OKconstp :Person = newEmployee ();
这听起来很简单,但有一些情况比其他情况更奇怪。
空类没有成员。在结构化类型系统中,没有成员的类型通常是任何其他类型的超类型。因此,如果你写一个空类(不要!),任何东西都可以用来代替它。
tsTry
classEmpty {}functionfn (x :Empty ) {// can't do anything with 'x', so I won't}// All OK!fn (window );fn ({});fn (fn );