TypeScript 入门第二部分 - 类和接口
类型别名
课程
接口
模块
枚举再探
返回函数
悬念
资源
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
这篇文章摘自我的博客,记得去看看,那里有更多最新内容哦😉
在上一篇文章中,我介绍了 TypeScript 以及它值得学习的原因。我涵盖了基本类型、顶级类型、联合体、函数、类型守卫等主题,所以如果您对这些术语感到陌生,我建议您先阅读上一篇文章。如果您已经了解,那就更好了,因为本文将大量引用上一篇文章中的知识。在本教程中,我们将探索一些更复杂、更有趣的 TypeScript 结构和功能。我将向您介绍接口、类以及其他一些 TypeScript 特性,这些特性肯定会提升您的开发体验、舒适度和 IDE 支持。事不宜迟,让我们开始吧!祝您学习愉快! 😁
类型别名
在第一篇文章中,我们探索了许多新的类型。我称之为组合类型的类型语法尤其冗长。想象一下,如果你需要反复使用某种联合类型,不仅需要输入大量代码,而且也不符合 DRY 原则,导致代码混乱。如何解决这个问题呢?TypeScript 提供了一些帮助——类型别名。顾名思义,类型别名允许你为指定的类型分配不同的名称。
type MyUnionType = string | number;
const myUnionVariable: MyUnionType = "str";
const myUnionVariable2: MyUnionType = 10;
类型别名就像一个常量,你可以将类型赋值给它。要自己指定一个类型别名,你需要使用 `type` 关键字,选择一个名称,然后为其赋值。😉 就像普通变量一样!之后,你就可以像引用普通类型一样,通过别名来引用你的类型了。不过,关于命名需要注意一点。通常的做法是类型名称以大写字母开头,这样可以将其与标准变量区分开来。
使用合适的类型别名,可以更好地为你的类型提供文档。例如,假设有一个字符串字面量的联合类型,为其指定别名就能提供更详细的描述。此外,IDE 应该能够识别你的别名,并在你使用时显示别名而不是冗长的联合类型名称。
课程
我预计到 2019 年,所有 JavaScript 开发者都会了解 ES6 和 ES-Next 是什么,以及它们带来了哪些特性。正如我在第一篇文章中提到的,TypeScript 是 ES-Next 的超集(静态类型系统),这意味着它的编译器可以将一些 ES-Next 的语法特性转译到旧版本的 ES 中,从而更好地兼容跨浏览器。这些特性包括类(在大多数现代浏览器中已经得到很好的支持)和装饰器(当时是第二阶段提案)。我不会专门介绍这些特性,因为它们可能已经广为人知,而且通常与 JavaScript 更相关。如果你想了解更多,可以点击这里和这里阅读相关内容。相反,我们将重点关注 TypeScript 为类添加的特性,因为,没错,这些特性非常多!😮
班级成员
我们都知道,在 TypeScript 中,所有东西都必须有类型,包括类成员。在使用this.语法访问任何成员之前,需要先声明该成员。
class MyClass {
myStringMember: string = 'str';
myBooleanMember?: boolean;
constructor() {
this.myStringMember; // 'str'
this.myNumberMember = 10; // error
}
}
如果未事先声明属性,将会收到访问错误。类成员的声明实际上就是在给定的类中指定其名称和类型,如上例所示。您还可以选择在声明成员时为其分配默认值?。此外,您还可以使用可选符号 `('可选')`,使成员成为非必需的。这两种方法都使得构造函数中无需为特定成员赋值。
修饰符
作为一种静态类型语言,TypeScript 借鉴了许多其他类似语言的思想,其中之一就是访问修饰符。要使用访问修饰符,需要在类成员之前指定相应修饰符的关键字。
class MyClass {
private myStringMember: string;
protected myNumberMember: number;
public constructor() {
this.myStringMember = 'str';
this.myNumberMember = 10;
}
}
你可以将这些修饰符用于属性、方法,甚至构造函数(但有一些限制)。务必记住,这些修饰符仅供 TypeScript 编译器和 IDE 使用,但由于 TypeScript 会被转译成 JavaScript,因此具有不同修饰符的成员之间没有任何区别。JavaScript 不提供任何更改类成员访问权限的选项,因此所有成员在输出的代码中都是公开可访问的。🤨
民众
如果没有直接指定修饰符,则使用此默认修饰符。它表示给定的成员可以公开访问,即在给定类的外部和内部都可以访问。
class MyClass {
public myStringMember: string = 'str';
public constructor() {
this.myStringMember; // 'str'
}
}
new MyClass().myStringMember; // 'str'
它也是可以应用于构造函数的两个修饰符之一(默认情况下是)。公共构造函数允许在代码中的任何位置实例化你的类。
私人的
私有修饰符将类成员的访问权限限制在类内部。在类外部访问它会抛出错误。这遵循面向对象编程的封装原则👏,允许你隐藏在给定作用域之外不需要的信息。
class MyClass {
private myStringMember: string = 'str';
constructor() {
this.myStringMember; // 'str'
}
}
new MyClass().myStringMember; // error
总的来说,这项技术非常有用。可惜的是,JavaScript 中没有直接等效的机制。虽然目前已经有人提出过相关提案,但就目前而言,闭包似乎是唯一的替代方案。这就是为什么在 TypeScript 编译器的输出中,所有成员都是公开可访问的。
受保护的
受保护修饰符介于私有修饰符和公共修饰符之间。受保护的成员可以在类及其所有派生类内部访问(与私有成员不同)。
class MyClass {
protected myStringMember: string = 'str';
protected constructor() {
this.myStringMember; // 'str'
}
}
class MyInheritedClass extends MyClass {
public constructor() {
super();
this.myStringMember; // 'str'
}
}
new MyClass(); // error
const instance = new MyInheritedClass();
instance.myStringMember; // error
上面的代码片段应该能让你更好地理解发生了什么。注意,`protected` 修饰符可以与构造函数一起使用。它实际上会使你的类不可实例化,这意味着你不能直接创建它的实例。你必须创建一个继承自前一个类的类(这样在继承的类中就可以访问受保护的构造函数),但构造函数是公共的。这虽然是个技巧,但实际上并不实用。如果你想要一个仅用于继承的类,那么使用抽象类可能更好,我们稍后会讨论抽象类。
再次强调,修饰符的概念对于之前使用过 Java 或 C# 等语言编程的人来说应该并不陌生。但是,由于我们这里讨论的是 JavaScript,它为我们改进软件架构带来了全新的可能性。😉
除了辅助功能修饰符之外,TypeScript 还提供了另外两个(TypeScript v3.3):`readonly`readonly和 `returnonly` static。虽然static`readonly` 是 JavaScript 的一部分(这并不奇怪),但`readonly`readonly不是。顾名思义,`readonly` 允许将特定成员指定为只读。因此,只有在构造函数中声明 `readonly` 时,该成员才能被赋值。
class MyClass {
readonly myStringMember: string = 'str';
constructor() {
this.myStringMember = 'string'
}
myMethod(): void {
this.myStringMember = 'str'; // error
}
}
readonly属性修饰符仅适用于属性(不适用于方法或构造函数),并且必须使用正确的关键字。此外,请记住,readonly 可以与其他辅助功能修饰符按特定顺序一起使用。
至于static修饰符,它的工作原理是使给定的成员在类本身而非其实例上可访问。此外,静态成员不能通过此修饰符访问,也不能被此修饰符访问。相反,您可以通过直接引用成员名称来访问类成员,例如 `this.static` MyClass。静态成员允许您定义跨实例的常量,或者将类用作各种方法的集合。
class MyClass {
static myStringMember: string = 'str';
constructor() {
this.myStringMember // error
MyClass.myStringMember // 'str'
}
static myMethod(): void {
this; // error
}
}
抽象类
在文章前面,我提到了抽象类。那么,抽象类是什么呢?简单来说,抽象类就是不能被实例化的类,它们只能作为其他继承类的引用。至于语法,抽象类引入的唯一新特性就是abstract关键字 `__init__`。它用于定义类本身及其成员。
abstract class MyAbstractClass {
abstract myAbstractMethod(): void;
abstract myAbstractStringMember: string;
constructor() {
this.myMethod();
}
myMethod() {
this.myAbstractMethod();
}
}
上面的例子展示了以(基本)正确的方式使用抽象类的全部潜力。我们已经知道 abstract 用于声明相应的类。但是,当 abstract 用于类成员时意味着什么呢?它表示被继承类需要自行实现的成员。如果找不到正确的实现,则会抛出错误。任何其他已实现的成员通常会被相应的类继承。🙂
class MyClass extends MyAbstractClass {
myAbstractStringMember: string = 'str';
myAbstractMethod(): void {
// code
};
}
new MyAbstractClass() // error
new MyClass().myAbstractStringMember; // 'str'
申报时间
声明类时,实际上你做了两件事——创建给定类的实例类型和所谓的构造函数。
创建实例类型允许您将变量的类型定义为特定类的实例。您可以像使用其他类型一样使用此类型,只需使用类名即可。
const instance: MyClass = new MyClass();
另一方面,构造函数是在使用关键字创建给定类的实例时调用的函数new。
但如果你想将构造函数本身而不是一个实例赋值给一个变量呢?在 JavaScript 中,你可以这样写:
const MyClassAlias = MyClass;
classAlias但是,在 TypeScript 中,`type`的实际类型是什么呢?这就用到了typeof`type` 关键字,它之前我们只知道它是一种类型守卫。它允许你获取任何 JavaScript 值的类型,以便后续使用。所以,要回答这个问题:
const MyClassAlias: typeof MyClass = MyClass;
const instance: MyClass = new ClassAlias();
最后,我们来看最后一个技巧。你多久会用构造函数参数来赋值类成员呢?这种情况非常常见,以至于 TypeScript 专门为此提供了一个快捷方式。你可以在参数前面加上任何可访问性或只读修饰符,这样你的参数就可以成为一个完整的类成员。是不是很有意思?😄
class MyClass {
constructor(public myStringMember: string) {}
myMethod(): void {
this.myStringMember;
}
}
接口
现在我们已经充分了解了 TypeScript 类,是时候探索接口了!🎉 接口是许多静态类型语言的黄金标准。它们允许你定义和操作值的“形状”,而不是值本身。
接口通常用于描述复杂结构(例如对象和类)的形状。它们指示最终结构需要具有哪些公开可用的属性/成员。要定义一个接口,必须使用interface关键字和正确的语法:
interface MyInterface {
readonly myStringProperty: string = 'str';
myNumberProperty?: number;
myMethodProperty(): void
}
在接口声明中,我们可以使用之前学过的 TypeScript 语法,特别是只读属性、可选属性和默认值。接口还可以包含我们未来结构体需要实现的方法。
接口的主要用途之一是作为一种类型。您可以使用已知的语法来使用它。
const myValue: MyInterface = {
myStringProperty: "str";
myMethodProperty() {
// code
}
}
接口还允许你描述诸如函数和类构造函数之类的值。但是,它们各自的语法有所不同:
interface MyFunctionInterface {
(myNumberArg: number, myStringArg: string): void;
}
interface MyClassInterface {
myStringMember: string;
}
interface MyClassConstructorInterface {
new (myNumberArg: number): MyClassInterface;
}
说到接口,你可以利用它们创建不同的类型,从而更好地展现JavaScript 的灵活性。这就是为什么你可以将上述接口与其他属性结合起来,创建所谓的混合类型。😉
interface MyHybridInterface {
(myNumberArg: number, myStringArg: string): void;
myNumberProperty: number;
myStringProperty: string;
}
例如,这个接口描述了一个具有两个附加属性的函数。这种模式可能不太常见,但在动态 JavaScript 中完全可行。
遗产
接口和类一样,可以相互扩展,也可以扩展类的属性!你可以使用简单的 `extends` 关键字语法,让你的接口扩展一个或多个接口(类不支持此功能)。在这种情况下,被扩展接口共享的属性会被合并成一个单独的属性。
interface MyCombinedInterface extends MyInterface, MyHybridInterface {
myBooleanProperty: boolean;
}
当接口继承一个类时,它会继承该类的所有成员,无论这些成员使用了什么访问修饰符。但是,访问修饰符的作用要到后面才会显现出来:当接口只能由提供私有成员的类或其派生类实现时,访问修饰符才会发挥作用。这是访问修饰符与接口交互的唯一情况。否则,接口本身仅描述值的格式,因此访问修饰符既不存在,也没有必要存在。🙂
interface MyCombinedInterface extends MyClass {
myBooleanProperty: boolean;
}
课程
接口和类之间有着特殊的联系。仅从它们的声明语法就能看出它们的相似之处。这是因为类可以实现接口。
class MyClass implements MyInterface {
myStringProperty: string = 'str';
myNumberProperty: number = 10;
}
通过使用implements关键字,您可以指定给定类必须实现特定接口中描述的所有属性。这样,您就可以更快速地定义变量。
const myValue: MyInterface = new MyClass();
还记得类构造函数接口吗?事情到这里就变得有点复杂了。我们之前讨论类的时候,我提到过,定义一个类时,你同时创建了实例类型(称为实例端)和构造函数(称为静态端)。使用类时,implements你实际上是在与实例端交互。你是在告诉编译器,该类的实例应该具有来自这个接口的属性。这就是为什么你不能这样写:
class MyClass implements MyClassConstructorInterface {
// code
}
这是因为这意味着该类的实例可以被自身实例化。正确的做法是,使用类构造函数接口来描述你需要的类,例如将其作为参数传递。或许一个完整的示例能更好地说明这一点。🚀
interface MyInterface {
myStringProperty: string;
}
interface MyClassConstructorInterface {
new (myNumberArg: number): MyInterface;
}
class MyClass implements MyInterface {
myStringProperty: string = 'str';
constructor(myNumberArg: number ){}
}
function generateMyClassInstance(ctor: MyClassConstructorInterface): MyInterface {
new ctor(10);
}
generateMyClassInstance(MyClass);
简要说明一下流程。首先,我们声明两个接口——一个用于实例端,定义实例的结构MyClass;另一个用于静态端,定义构造函数的结构。然后,我们使用正确的 implements 语句定义类。最后,我们利用该接口MyClassConstructorInterface定义所需的类构造函数(静态端)的结构,该构造函数可以传递给我们的函数,以便稍后实例化。
模块
这里简单提一下。📓 想必您现在已经熟悉 ES6 模块的概念了吧?在 TypeScript 中,除了普通的 JavaScript 值之外,标准的 ` [import](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)/`[export](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export)关键字还可以用于类型别名、枚举、接口等等。这使得您可以将代码合理地拆分成更小、更易于维护的模块。语法和一般规则保持不变。
export interface MyInterface {
myStringProperty: string = 'str';
myNumberProperty?: number;
}
枚举再探
在上一篇文章中,我们讨论了枚举如何为数值数据赋予更友好的名称。但不出所料,枚举的功能远不止于此。😃
除了数字之外,枚举还可以由字符串组成。在这种情况下,每个成员都必须被赋予一个固定的字符串值。所有其他与枚举相关的规则仍然适用。
enum MyStringEnum {
A = 'str1',
B = 'str2',
C = 'str3',
}
理论上,如果所有成员都直接赋值,那么你可以在枚举中自由混合使用字符串和数值。但实际上并没有太多应用场景。
枚举类型也可以在运行时作为类似对象的结构使用。此外,枚举成员不仅可以赋静态值,还可以赋计算值。因此,下面的赋值语句完全正确。
const myNumber: number = 20;
enum MyEnum {
X = myNumber * 10,
Y
};
const myObject: {X: number, Y: number} = MyEnum;
编译后,枚举会以 JS 对象的形式存在。但是,如果您希望枚举仅作为常量值的集合,则可以使用 const 关键字轻松实现。
const enum MyEnum {
X,
Y
}
在这样的常量枚举中,你不能像以前那样包含计算成员。这些枚举会在编译期间被移除,因此在它们被引用的地方只会留下常量值。
返回函数
我们已经对函数进行了很多讨论。但是,因为我们想了解更多,所以是时候看看一些更复杂的方面了。😉
默认值
与类成员类似,函数参数也可以设置默认值。函数可以有多个带有默认值的参数,但不能有任何未指定默认值的必需参数。只有当没有传递任何参数时,才会使用默认值。
function myFunc(myNumberArg: number, myDefaultStringArg: string = 'str') {
// code
}
myFunc(10);
myFunc(10, 'string');
这
.bind()随着ES6 中箭头函数和更完善的方法规范的引入,this函数内部的类型处理变得更加容易。但是,如何为this普通函数指定类型呢?除非你使用 `__init__`.bind()或类似方法,否则 TypeScript 的内置类型推断功能通常可以很好地处理类型问题。否则,你需要指定this 参数类型。
type Scope = {myString: string, myNumber: number};
function myFunc(this: Scope, myStringArg: string = 'str') {
this.myString;
this.myNumber;
}
myFunc(); // error
myFunc.bind({myString: 'str', myNumber: 'number'});
通过this提供参数,TS 编译器可以确保函数的当前上下文正确,并在其他情况下抛出错误。
对于箭头函数,没有this参数选项。箭头函数不能绑定,因为它们使用预先赋值的 this 值。因此,任何尝试为 this 参数赋值的操作都会抛出错误。
过载
函数重载允许你定义名称相同但参数不同的函数。当你需要在一个函数中接受不同类型的参数并分别处理它们时,通常会使用函数重载。
function myFunc(myArg: number): string;
function myFunc(myArg: string): number;
function myFunc(myArg): any {
if(typeof myArg === 'number'){
return 'str';
}
if(typeof myArg === 'string'){
return 10;
}
}
声明重载时,只需提供多个函数签名,然后使用更通用的类型(例如示例中的 `any`)定义实际函数。编译器随后会选择正确的重载并向 IDE 提供相应的信息。当然,同样的技术也可以用于类中。
其余参数
ES6 带来的另一个热门特性是剩余参数和解构运算符。TypeScript 对这两个特性都提供了良好的支持。TypeScript 允许你像定义其他参数一样定义剩余参数的类型:
function myFunc(myNumberArg: number, ...myRestStringArg: string[]) {
// code
}
myFunc(10, 'a', 'b', 'c');
至于解构,TS 类型推断完全可以胜任。
悬念
哇,我们讲的内容真不少,是不是?有了类和接口,你现在就可以开始用 TypeScript 自己写一些面向对象编程 (OOP) 了。信不信由你,静态类型语言在运用 OOP 及其原则方面要好得多。不过,还有很多东西要讲。我们还没谈到泛型、索引类型、声明合并以及其他更复杂的内容。所以,请关注我的Twitter和Facebook 主页,获取更多相关内容。另外,如果你喜欢这篇文章,请分享 一下,让更多人了解 TypeScript 和这个博客!😅 最后,别忘了在下方留言,说说你接下来想看到什么内容!
就先这样吧……暂时就这些。👏
资源
现在你对 TypeScript 有了一些了解,是时候拓展你的知识了。去阅读、编写代码、学习吧,然后回来阅读第三部分!😉
- TypeScript 官方文档来自typescriptlang.org;
- 从rachelappel.com使用 TypeScript 编写面向对象的 JavaScript;
- 来自devhints.io的TypeScript 速查表;