- 阐明:目前网上没有 TypeScript 最新官网文档的中文翻译,所以有了这么一个翻译打算。因为我也是 TypeScript 的初学者,所以无奈保障翻译百分之百精确,若有谬误,欢送评论区指出;
- 翻译内容:暂定翻译内容为 TypeScript Handbook,后续有空会补充翻译文档的其它局部;
- 我的项目地址:TypeScript-Doc-Zh,如果对你有帮忙,能够点一个 star ~
本章节官网文档地址:Classes
背景导读:类(MDN)
类
TypeScript 为 ES2015 引入的 class
关键字提供了全面的反对。
就像其它的 JavaScript 语言个性一样,TypeScript 也为类提供了类型注解和其它语法,以帮忙开发者示意类和其它类型之间的关系。
类成员
这是一个最根本的类 —— 它是空的:
class Point {}
这个类目前没有什么用,所以咱们给它增加一些成员吧。
字段
申明字段相当于是给类增加了一个公共的、可写的属性:
class Point { x: number; y: number;}const pt = new Point()pt.x = 0;pt.y = 0;
和其它个性一样,这里的类型注解也是可选的,但如果没有指定类型,则会隐式采纳 any
类型。
字段也能够进行初始化,初始化过程会在类实例化的时候主动进行:
class Point { x = 0; y = 0;} const pt = new Point();// 打印 0, 0console.log(`${pt.x}, ${pt.y}`);
就像应用 const
、let
和 var
一样,类属性的初始化语句也会被用于进行类型推断:
const pt = new Point();pt.x = "0";// Type 'string' is not assignable to type 'number'.
--strictPropertyInitialization
配置项 strictPropertyInitialization 用于管制类的字段是否须要在结构器中进行初始化。
class BadGreeter { name: string; ^// Property 'name' has no initializer and is not definitely assigned in the constructor.}class GoodGreeter { name: string; constructor() { this.name = "hello"; }}
留神,字段须要在结构器本身外部进行初始化。TypeScript 不会剖析在结构器中调用的办法以检测初始化语句,因为派生类可能会重写这些办法,导致初始化成员失败。
如果你保持要应用除了结构器之外的办法(比方应用一个内部库填充类的内容)去初始化一个字段,那么你能够应用确定赋值断言运算符 !
:
class OKGreeter { // 没有初始化,但不会报错 name!: string;}
readonly
字段能够加上 readonly
修饰符作为前缀,以避免在结构器里面对字段进行赋值。
class Greeter { readonly name: string = "world"; constructor(otherName?: string) { if (otherName !== undefined) { this.name = otherName; } } err() { this.name = "not ok"; ^// Cannot assign to 'name' because it is a read-only property. }}const g = new Greeter();g.name = "also not ok"; ^// Cannot assign to 'name' because it is a read-only property.
结构器
类的结构器和函数很像,你能够给它的参数增加类型注解,能够应用参数默认值或者是函数重载:
class Point { x: number; y: number; // 应用了参数默认值的失常签名 constructor(x = 0, y = 0) { this.x = x; this.y = y; }}class Point { // 应用重载 constructor(x: number, y: string); constructor(s: string); constructor(xs: any, y?: any) { // TBD }}
类的结构器签名和函数签名只有一点区别:
- 结构器不能应用类型参数 —— 类型参数属于类申明的局部,稍后咱们会进行学习
- 结构器不能给返回值增加类型注解 —— 它返回的类型始终是类实例的类型
super
调用
和 JavaScript 一样,如果你有一个基类和一个派生类,那么在派生类中应用 this.
拜访类成员之前,必须先在结构器中调用 super();
:
class Base { k = 4;} class Derived extends Base { constructor() { // ES5 下打印出谬误的值,ES6 下报错 console.log(this.k); ^// 'super' must be called before accessing 'this' in the constructor of a derived class. super(); }}
在 JavaScript 中,遗记调用 super
是一个常见的谬误,但 TypeScript 会在必要时给你揭示。
办法
类的属性可能是一个函数,这时候咱们称其为办法。办法和函数以及结构器一样,也能够应用各种类型注解:
class Point { x = 10; y = 10; scale(n: number): void { this.x *= n; this.y *= n; }}
除了规范的类型注解之外,TypeScript 没有给办法增加什么新的货色。
留神,在办法体中,必须通过 this.
能力拜访到类的字段和其它办法。在办法体中应用不合规的名字,将会被视为是在拜访邻近作用域中的变量:
let x: number = 0; class C { x: string = "hello"; m() { // 上面这句是在试图批改第一行的 x,而不是类的属性 x = "world"; ^ // Type 'string' is not assignable to type 'number'. }}
Getters/Setters
类也能够有拜访器:
class C { _length = 0; get length(){ return this._length; } set length(value){ this._length = value; }}
留神:在 JavaScript 中,一个没有额定逻辑的 get/set 对是没有什么作用的。如果在执行 get/set 操作的时候不须要增加额定的逻辑,那么只须要将字段裸露为公共字段即可。
对于拜访器,TypeScript 有一些非凡的推断规定:
- 如果
get
存在而set
不存在,那么属性会主动成为只读属性 - 如果没有指定 setter 参数的类型,那么会基于 getter 返回值的类型去推断参数类型
- getter 和 setter 必须具备雷同的成员可见性。
从 TypeScript 4.3 开始,拜访器的 getter 和 setter 能够应用不同的类型。
class Thing { _size = 0; get size(): number { return this._size; } set size(value: string | number | boolean) { let num = Number(value); // 不容许应用 NaN、Infinity 等 if (!Number.isFinite(num)) { this._size = 0; return; } this._size = num; }}
索引签名
类能够申明索引签名,其工作形式和其它对象类型的索引签名一样:
class MyClass { [s: string]: boolean | ((s: string) => boolean); check(s: string) { return this[s] as boolean; }}
因为索引签名类型也须要捕捉办法的类型,所以要无效地应用这些类型并不容易。通常状况下,最好将索引数据存储在另一个地位,而不是类实例自身。
类继承
和其它面向对象语言一样,JavaScript 中的类能够继承自基类。
implements
子句
你能够应用一个 implements
子句去查看类是否合乎某个特定的接口。如果类没有正确地实现这个接口,那么就会抛出一个谬误:
interface Pingable { ping(): void;} class Sonar implements Pingable { ping() { console.log("ping!"); }} class Ball implements Pingable { ^/*Class 'Ball' incorrectly implements interface 'Pingable'. Property 'ping' is missing in type 'Ball' but required in type 'Pingable'.*/ pong() { console.log("pong!"); }}
类能够实现多个接口,比方 class C implements A,B {
。
注意事项
有个要点须要了解,那就是 implements
子句只是用于查看类是否能够被视为某个接口类型,它齐全不会扭转类的类型或者它的办法。常见的谬误是认为 implements
子句会扭转类的类型 —— 实际上是不会的!
interface Checkable { check(name: string): boolean;} class NameChecker implements Checkable { check(s) { ^//Parameter 's' implicitly has an 'any' type. // 留神这里不会抛出谬误 return s.toLowercse() === "ok"; ^ // any }}
在这个例子中,咱们可能会认为 s
的类型会受到接口中 check
的 name: string
参数的影响。但实际上不会 —— implements
子句不会对类内容体的查看以及类型推断产生任何影响。
同理,实现一个带有可选属性的接口,并不会创立该属性:
interface A { x: number; y?: number;}class C implements A { x = 0;}const c = new C();c.y = 10; ^// Property 'y' does not exist on type 'C'.
extends
子句
类能够继承自某个基类。派生类领有基类的所有属性和办法,同时也能够定义额定的成员。
class Animal { move() { console.log("Moving along!"); }} class Dog extends Animal { woof(times: number) { for (let i = 0; i < times; i++) { console.log("woof!"); } }} const d = new Dog();// 基类办法d.move();// 派生类办法d.woof(3);
重写办法
派生类也能够重写基类的字段或者属性。你能够应用 super.
语法拜访基类的办法。留神,因为 JavaScript 的类只是一个简略的查找对象,所以不存在“父类字段”的概念。
TypeScript 强制认为派生类总是基类的一个子类。
比方,上面是一个非法的重写办法的例子:
class Base { greet() { console.log("Hello, world!"); }} class Derived extends Base { greet(name?: string) { if (name === undefined) { super.greet(); } else { console.log(`Hello, ${name.toUpperCase()}`); } }} const d = new Derived();d.greet();d.greet("reader");
很重要的一点是,派生类会遵循基类的束缚。通过一个基类援用去援用一个派生类,是很常见(并且总是非法的!)的一种做法:
// 通过一个基类援用去命名一个派生类实例const b: Base = d;// 没有问题b.greet();
如果派生类 Derived
没有遵循基类 Base
的束缚,会怎么样呢?
class Base { greet() { console.log("Hello, world!"); }} class Derived extends Base { // 让这个参数成为必选参数 greet(name: string) { ^ /*Property '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'.*/ console.log(`Hello, ${name.toUpperCase()}`); }}
如果忽视谬误并编译代码,那么上面的代码执行后会报错:
const b: Base = new Derived();// 因为 name 是 undefined,所以报错b.greet();
初始化程序
JavaScript 类的初始化程序在某些状况下可能会让你感到意外。咱们看看上面的代码:
class Base { name = "base"; constructor() { console.log("My name is " + this.name); }} class Derived extends Base { name = "derived";} // 打印 base 而不是 derivedconst d = new Derived();
这里产生了什么事呢?
依据 JavaScript 的定义,类初始化的程序是:
- 初始化基类的字段
- 执行基类的结构器
- 初始化派生类的字段
- 执行派生类的结构器
这意味着,因为基类结构器执行的时候派生类的字段尚未进行初始化,所以基类结构器只能看到本人的 name
值。
继承内置类型
留神:如果你不打算继承诸如 Array、Error、Map 等内置类型,或者你的编译指标显式设置为 ES6/ES2015 或者更高的版本,那么你能够跳过这部分的内容。
在 ES2015 中,返回实例对象的结构器会隐式地将 this
的值替换为 super(...)
的任意调用者。有必要让生成的结构器代码捕捉 super(...)
的任意潜在的返回值,并用 this
替换它。
因而,Error
、Array
等的子类可能无奈如预期那样失效。这是因为诸如 Error
、Array
这样的构造函数应用了 ES6 的 new.target
去调整原型链,然而,在 ES5 中调用结构器函数的时候,没有相似的办法能够确保 new.target
的值。默认状况下,其它底层编译器通常也具备雷同的限度。
对于一个像上面这样的子类:
class MsgError extends Error { constructor(m: string) { super(m); } sayHello() { return "hello " + this.message; }}
你可能会发现:
- 调用子类之后返回的实例对象,其办法可能是
undefined
,所以调用sayHello
将会抛出谬误 - 子类实例和子类之间的
instanceof
可能被毁坏,所以(new MsgError()) instanceof MsgError
将会返回false
。
举荐的做法是,在任意的 super(...)
调用前面手动地调整原型链:
class MsgError extends Error { constructor(m: string) { super(m); // 显式设置原型链 Object.setPrototypeOf(this, MsgError.prototype); } sayHello() { return "hello " + this.message; }}
不过,MsgError
的任意子类也须要手动设置原型。对于不反对 Object.setPrototypeOf 的运行时,你能够改用 __proto__
。
蹩脚的是,这些变通方法在 IE10 或者更旧的版本上无奈应用.aspx)。你能够手动将原型上的办法复制到实例上(比方将 MsgError.prototype
的办法复制给 this
),但原型链自身无奈被修复。
成员可见性
你能够应用 TypeScript 管制特定的办法或属性是否在类的里面可见。
public
类成员的默认可见性是私有的(public
)。私有成员随处能够拜访:
class Greeter { public greet(){ console.log('hi!'); }}const g = new Greeter();g.greet();
因为成员的可见性默认就是私有的,所以你不须要在类成员后面进行显式申明,但出于代码标准或者可读性的思考,你也能够这么做。
protected
受爱护(protected
)成员只在类的子类中可见。
class Greeter { public greet() { console.log("Hello, " + this.getName()); } protected getName() { return "hi"; }} class SpecialGreeter extends Greeter { public howdy() { // 这里能够拜访受爱护成员 console.log("Howdy, " + this.getName()); }}const g = new SpecialGreeter();g.greet(); // OKg.getName(); ^// Property 'getName' is protected and only accessible within class 'Greeter' and its subclasses.
公开受爱护成员
派生类须要遵循其基类的束缚,但能够抉择公开具备更多功能的基类的子类。这包含了让受爱护成员变成私有成员:
class Base { protected m = 10;}class Derived extends Base { // 没有修饰符,所以默认可见性是私有的 m = 15;}const d = new Dervied();console.log(d.m); // OK
留神 Dervied
曾经能够自在读写成员 m
了,所以这么写并不会扭转这种状况的“安全性”。这里须要留神的要点是,在派生类中,如果咱们无心公开其成员,那么须要增加 protected
修饰符。
跨层级拜访受爱护成员
对于通过一个基类援用拜访受爱护成员是否非法,不同的 OOP 语言之间存在争议:
class Base { protected x: number = 1;}class Derived1 extends Base { protected x: number = 5;}class Derived2 extends Base { f1(other: Derived2) { other.x = 10; } f2(other: Base) { other.x = 10; ^// Property 'x' is protected and only accessible through an instance of class 'Derived2'. This is an instance of class 'Base'. }}
举个例子,Java 认为上述代码是非法的,但 C# 和 C++ 则认为上述代码是不非法的。
TypeScript 也认为这是不非法的,因为只有在 Derived2
的子类中拜访 Derived2
的 x
才是非法的,但 Derived1
并不是 Derived2
的子类。而且,如果通过 Derived1
援用拜访 x
就曾经是不非法的了(这的确应该是不非法的!),那么通过基类援用拜访它也同样应该是不非法的。
对于 C# 为什么会认为这段代码是不非法的,能够浏览这篇文章理解更多信息:为什么我无奈在一个派生类中去拜访一个受爱护成员?
private
private
和 protected
一样,但申明了 private
的公有成员即便在子类中也无奈被拜访到:
class Base { private x = 0;}const b = new Base();// 无奈在类里面拜访console.log(b.x);// Property 'x' is private and only accessible within class 'Base'.class Derived extends Base { showX() { // 无奈在子类中拜访 console.log(this.x); ^ // Property 'x' is private and only accessible within class 'Base'. }}
因为公有成员对派生类不可见,所以派生类无奈进步其可见性:
class Base { private x = 0;}class Dervied extends Base {/*Class 'Derived' incorrectly extends base class 'Base'. Property 'x' is private in type 'Base' but not in type 'Derived'. */ x = 1;}
跨实例拜访公有成员
对于同一个类的不同实例相互拜访对方的公有成员是否非法,不同的 OOP 语言之间存在争议。Java、C#、C++、Swift 和 PHP 容许这么做,但 Ruby 则认为这样做是不非法的。
TypeScript 容许跨实例拜访公有成员:
class A { private x = 10; public sameAs(other: A) { // 不会报错 return other.x === this.x; }}
注意事项
和 TypeScript 类型零碎中的其它货色一样,private
和 protected
只在类型查看期间失效。
这意味着 JavaScript 运行时的一些操作,诸如 in
或者简略的属性查找依然能够拜访公有成员或者受爱护成员:
class MySafe { private serectKey = 123345;}// 在 JavaScript 文件中会打印 12345const s = new MySafe();console.log(s.secretKey);
而即便是在类型查看期间,咱们也能够通过方括号语法去拜访公有成员。因而,在进行诸如单元测试这样的操作时,拜访公有字段会比拟容易,但毛病就是这些字段是“弱公有的”,无奈保障严格意义上的公有性。
class MySafe { private secretKey = 12345;} const s = new MySafe(); // 在类型查看期间,不容许这样拜访公有成员console.log(s.secretKey); ^// Property 'secretKey' is private and only accessible within class 'MySafe'. // 然而能够通过方括号语法拜访console.log(s["secretKey"]);
和 TypeScript 用 private
申明的公有成员不同,JavaScript 用 #
申明的公有字段在编译之后也依然是公有的,并且没有提供像下面那样的方括号语法用于拜访公有成员,所以 JavaScript 的公有成员是“强公有的”。
class Dog { #barkAmount = 0; personality = 'happy'; constructor() {}}
以上面这段 TypeScript 代码为例:
"use strict";class Dog { #barkAmount = 0; personality = "happy"; constructor() { }}
把它编译为 ES2021 或者更低版本的代码之后,TypeScript 会应用 WeakMap 代替 #
。
"use strict";var _Dog_barkAmount;class Dog { constructor() { _Dog_barkAmount.set(this, 0); this.personality = "happy"; }}_Dog_barkAmount = new WeakMap();
如果你须要爱护类中的值不被歹意批改,那么你应该应用提供了运行时公有性保障的机制,比方闭包、WeakMap 或者公有字段等。留神,这些在运行时增加的公有性查看可能会影响性能。
动态成员
背景导读:动态成员(MDN)
类能够领有动态(static
)成员。这些成员和类的特定实例无关,咱们能够通过类结构器对象自身拜访到它们:
class MyClass { static x = 0; static printX(){ console.log(MyClass.x); }}console.log(MyClass.x);MyClass.printX();
动态成员也能够应用 public
、protected
和 private
等可见性修饰符:
class MyClass { private static x = 0;}console.log(MyClass.x); ^// Property 'x' is private and only accessible within class 'MyClass'.
动态成员也能够被继承:
class Base { static getGreeting() { return "Hello world"; }}class Derived extends Base { myGreeting = Derived.getGreeting();}
非凡的动态成员名字
重写 Function
原型的属性通常是不平安/不可能的。因为类自身也是一个能够通过 new
调用的函数,所以无奈应用一些特定的动态成员名字。诸如 name
、length
和 call
这样的函数属性无奈作为动态成员的名字:
class S { static name = 'S!'; ^// Static property 'name' conflicts with built-in property 'Function.name' of constructor function 'S'. }
为什么没有动态类?
TypeScript(和 JavaScript)并没有像 C# 和 Java 那样提供动态类这种构造。
C# 和 Java 之所以须要动态类,是因为这些语言要求所有的数据和函数必须放在一个类中。因为在 TypeScirpt 中不存在这个限度,所以也就不须要动态类。只领有单个实例的类在 JavaScript/TypeScirpt 中通常用一个一般对象示意。
举个例子,在 TypeScript 中咱们不须要“动态类”语法,因为一个惯例的对象(甚至是顶层函数)也能够实现雷同的工作:
// 不必要的动态类class MyStaticClass { static doSomething() {}}// 首选(计划一)function doSomething() {}// 首选(计划二)const MyHelperObject = { dosomething() {},};
类中的动态块
动态块容许你编写一系列申明语句,它们领有本人的作用域,并且能够拜访蕴含类中的公有字段。这意味着咱们可能编写初始化代码,这些代码蕴含了申明语句,不会有变量透露的问题,并且齐全能够拜访类的外部。
class Foo { static #count = 0; get count(){ return Foo.#count; } static { try { const lastInstances = loadLastInstances(); Foo.#count += lastInstances.length; } catch {} }}
泛型类
类和接口一样,也能够应用泛型。当用 new
实例化一个泛型类的时候,它的类型参数就像在函数调用中那样被推断进去:
class Box<Type> { contents: Type; constructor(value: Type){ this.contents = value; }}const b = new Box('hello!'); ^ // const b: Box<string>
类能够像接口那样应用泛型束缚和默认值。
动态成员中的类型参数
上面的代码是不非法的,但起因可能不那么显著:
class Box<Type> { static defaultValue: Type; ^// Static members cannot reference class type parameters. }
记住,类型在编译后总是会被齐全抹除的!在运行时,只有一个 Box.defaultValue
属性插槽。这意味着设置 Box<string>.defaultValue
(如果能够设置的话)也会扭转 Box<number>.defaultValue
—— 这是不行的。泛型类的动态成员永远都不能引用类的类型参数。
类的运行时 this
有个要点须要记住,那就是 TypeScript 不会扭转 JavaScript 的运行时行为。而家喻户晓,JavaScript 领有一些非凡的运行时行为。
JavaScript 对于 this
的解决的确是很不寻常:
class MyClass { name = "MyClass"; getName() { return this.name; }}const c = new MyClass();const obj = { name: "obj", getName: c.getName,}; // 打印 "obj" 而不是 "MyClass"console.log(obj.getName());
长话短说,默认状况下,函数中 this
的值取决于函数是如何被调用的。在这个例子中,因为咱们通过 obj
援用去调用函数,所以它的 this
的值是 obj
,而不是类实例。
这通常不是咱们冀望的后果!TypeScript 提供了一些办法让咱们能够缩小或者避免这种谬误的产生。
箭头函数
如果你的函数在被调用的时候常常会失落 this
上下文,那么最好应用箭头函数属性,而不是办法定义:
class MyClass { name = 'MyClass'; getName = () => { return this.name; };}const c = new MyClass();const g = c.getName;// 打印 MyClass console.log(g());
这种做法有一些利弊衡量:
- 在运行时能够保障
this
的值是正确的,即便对于那些没有应用 TypeScript 进行查看的代码也是如此 - 这样会占用更多内存,因为以这种形式定义的函数,会导致每个类实例都有一份函数正本
- 你无奈在派生类中应用
super.getName
,因为在原型链上没有入口能够去获取基类的办法
this
参数
在 TypeScript 的办法或者函数定义中,第一个参数的名字如果是 this
,那么它有非凡的含意。这样的参数在编译期间会被抹除:
// TypeScript 承受 this 参数function fn(this: SomeType, x: number) { /* ... */}// 输入得 JavaScript function fn(x) { /* ... */}
TypeScript 会查看传入 this
参数的函数调用是否位于正确的上下文中。这里咱们没有应用箭头函数,而是给办法定义增加了一个 this
参数,以动态的形式确保办法能够被正确调用:
class MyClass { name = "MyClass"; getName(this: MyClass) { return this.name; }}const c = new MyClass();// OKc.getName(); // 报错const g = c.getName;console.log(g());// The 'this' context of type 'void' is not assignable to method's 'this' of type 'MyClass'.
这种办法的利弊衡量和下面应用箭头函数的办法相同:
- JavaScript 的调用方可能依然会在没有意识的状况下谬误地调用类办法
- 只会给每个类定义调配一个函数,而不是给每个类实例调配一个函数
- 依然能够通过
super
调用基类定义的办法
this
类型
在类中,名为 this
的非凡类型能够动静地援用以后类的类型。咱们看一下它是怎么发挥作用的:
class Box { contents: string = ""; set(value: string){ ^ // (method) Box.set(value: string): this this.contents = value; return this; }}
这里,TypeScript 将 set
的返回值类型推断为 this
,而不是 Box
。当初咱们来创立一个 Box
的子类:
class ClearableBox extends Box { clear() { this.contents = ""; }}const a = new ClearableBox();const b = a.set("hello"); ^// const b: ClearableBox
你也能够在参数的类型注解中应用 this
:
class Box { content: string = ""; sameAs(other: this) { return other.content === this.content; }}
这和应用 other: Box
是不一样的 —— 如果你有一个派生类,那么它的 sameAs
办法将只会承受该派生类的其它实例:
class Box { content: string = ""; sameAs(other: this) { return other.content === this.content; }} class DerivedBox extends Box { otherContent: string = "?";} const base = new Box();const derived = new DerivedBox();derived.sameAs(base); ^/*Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'. Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.*/
基于 this
的类型爱护
你能够在类和接口的办法的返回值类型注解处应用 this is Type
。该语句和类型膨胀(比如说 if
语句)一起应用的时候,指标对象的类型会被膨胀为指定的 Type
。
class FileSystemObject { isFile(): this is FileRep { return this instanceof FileRep; } isDirectory(): this is Directory { return this instanceof Directory; } isNetworked(): this is Networked & this { return this.networked; } constructor(public path: string, private networked: boolean) {}} class FileRep extends FileSystemObject { constructor(path: string, public content: string) { super(path, false); }} class Directory extends FileSystemObject { children: FileSystemObject[];} interface Networked { host: string;} const fso: FileSystemObject = new FileRep("foo/bar.txt", "foo"); if (fso.isFile()) { fso.content; ^ // const fso: FileRep} else if (fso.isDirectory()) { fso.children; ^ // const fso: Directory} else if (fso.isNetworked()) { fso.host; ^ // const fso: Networked & FileSystemObject}
基于 this
的类型爱护的常见用例是容许特定字段的提早验证。以上面的代码为例,当 hasValue
被验证为 true 的时候,能够移除 Box
中为 undefined
的 value
值:
class Box<T> { value?: T; hasValue(): this is { value: T } { return this.value !== undefined; }} const box = new Box();box.value = "Gameboy"; box.value; ^ // (property) Box<unknown>.value?: unknown if (box.hasValue()) { box.value; ^ // (property) value: unknown}
参数属性
TypeScript 提供了一种非凡的语法,能够将结构器参数转化为具备雷同名字和值的类属性。这种语法叫做参数属性,实现形式是在结构器参数后面加上 public
、private
、protected
或者 readonly
等其中一种可见性修饰符作为前缀。最终的字段将会取得这些修饰符:
class Params { constructor( public readonly x: number, protected y: number, private z: number ) { // 没有必要编写结构器的函数体 } }const a = new Params(1,2,3);console.log(a.x); ^ // (property) Params.x: numberconsole.log(a.z); ^// Property 'z' is private and only accessible within class 'Params'.
类表达式
背景导读:类表达式(MDN)
类表达式和类申明十分类似。惟一的不同在于,类表达式不须要名字,但咱们依然能够通过任意绑定给类表达式的标识符去援用它们:
const someClass = class<Type> { content: Type; constructor(value: Type) { this.content = value; }};const m = new someClass("Hello, world"); ^ // const m: someClass<string>
抽象类和成员
在 TypeScript 中,类、办法和字段可能是形象的。
形象办法或者形象字段在类中没有对应的实现。这些成员必须存在于一个无奈间接被实例化的抽象类中。
抽象类的角色是充当一个基类,让其子类去实现所有的形象成员。当一个类没有任何形象成员的时候,咱们就说它是具体的。
来看一个例子:
abstract class Base { abstract getName(): string; printName(){ console.log("Hello, " + this.getName()); }}const b = new Base();// Cannot create an instance of an abstract class.
因为 Base
是一个抽象类,所以咱们不能应用 new
去实例化它。相同地,咱们须要创立一个派生类,让它去实现形象成员:
class Derived extends Base { getName() { rteurn "world"; }}const d = new Derived();d.printName();
留神,如果咱们遗记实现基类的形象成员,那么会抛出一个谬误:
class Derived extends Base { ^// Non-abstract class 'Derived' does not implement inherited abstract member 'getName' from class 'Base'. // 遗记实现形象成员}
形象结构签名
有时候你想要承受一个类结构器函数作为参数,让它产生某个类的实例,并且这个类是从某个抽象类派生过去的。
举个例子,你可能想要编写上面这样的代码:
function greet(ctor: typeof Base) { const instance = new ctor();// Cannot create an instance of an abstract class. instance.printName();}
TypeScript 会正确地通知你,你正试图实例化一个抽象类。毕竟,依据 greet
的定义,编写这样的代码理当是齐全非法的,它最终会结构一个抽象类的实例:
// 不行!greet(Base);
但它实际上会报错。所以,你编写的函数所承受的参数应该带有一个结构签名:
function greet(ctor: new () => Base) { const instance = new ctor(); instance.printName();}greet(Derived);greet(Base); ^ /*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.*/
当初 TypeScript 能够正确地告知你哪个类结构器函数能够被调用了 —— Derived
能够被调用,因为它是一个具体类,而 Base
不能被调用,因为它是一个抽象类。
类之间的分割
在大多数状况下,TypeScript 中的类是在结构上进行比拟的,就跟其它类型一样。
举个例子,上面这两个类能够相互代替对方,因为它们在结构上是截然不同的:
class Point1 { x = 0; y = 0;} class Point2 { x = 0; y = 0;} // OKconst p: Point1 = new Point2();
相似地,即便没有显式申明继承关系,类和类之间也能够存在子类分割:
class Person { name: string; age: number;} class Employee { name: string; age: number; salary: number;} // OKconst p: Person = new Employee();
这听起来很简略易懂,但还有一些状况会比拟奇怪。
空类没有成员。在一个结构化的类型零碎中,一个没有成员的类型通常是任何其它类型的超类。所以如果你编写了一个空类(不要这么做!),那么你能够用任何类型去代替它:
class Empty {}function fn(x: Empty) { // 无奈对 x 执行任何操作,所以不倡议这么写}// 这些参数都是能够传入的!fn(window);fn({});fn(fn);