发布于 2026-01-06 2 阅读
0

TypeScript 入门第二部分 - 类和接口 类型别名 类 接口 模块 枚举回顾 回到函数 悬念 资源 DEV 的全球展示挑战赛 由 Mux 呈现:展示你的项目!

TypeScript 入门第二部分 - 类和接口

类型别名

课程

接口

模块

枚举再探

返回函数

悬念

资源

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

这篇文章摘自我的博客,记得去看看,那里有更多最新内容哦😉

上一篇文章中,我介绍了 TypeScript 以及它值得学习的原因。我涵盖了基本类型顶级类型联合体函数类型守卫等主题,所以如果您对这些术语感到陌生,我建议您先阅读上一篇文章。如果您已经了解,那就更好了,因为本文将大量引用上一篇文章中的知识。在本教程中,我们将探索一些更复杂、更有趣的 TypeScript 结构和功能。我将向您介绍接口以及其他一些 TypeScript 特性,这些特性肯定会提升您的开发体验、舒适度和 IDE 支持。事不宜迟,让我们开始吧!祝您学习愉快! 😁

类型别名

在第一篇文章中,我们探索了许多新的类型。我称之为组合类型的类型语法尤其冗长。想象一下,如果你需要反复使用某种联合类型,不仅需要输入大量代码,而且也不符合 DRY 原则,导致代码混乱。如何解决这个问题呢?TypeScript 提供了一些帮助——类型别名。顾名思义,类型别名允许你为指定的类型分配不同的名称。

type MyUnionType = string | number;

const myUnionVariable: MyUnionType = "str";
const myUnionVariable2: MyUnionType = 10;
Enter fullscreen mode Exit fullscreen mode

类型别名就像一个常量,你可以将类型赋值给它。要自己指定一个类型别名,你需要使用 `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
    }
}
Enter fullscreen mode Exit fullscreen mode

如果未事先声明属性,将会收到访问错误。类成员的声明实际上就是在给定的类中指定其名称和类型,如上例所示。您还可以选择在声明成员时为其分配默认值?。此外,您还可以使用可选符号 `('可选')`,使成员成为非必需的。这两种方法都使得构造函数中无需为特定成员赋值。

修饰符

作为一种静态类型语言,TypeScript 借鉴了许多其他类似语言的思想,其中之一就是访问修饰符。要使用访问修饰符,需要在类成员之前指定相应修饰符的关键字。

class MyClass {
    private myStringMember: string;
    protected myNumberMember: number;

    public constructor() {
        this.myStringMember = 'str';
        this.myNumberMember = 10;
    }
}
Enter fullscreen mode Exit fullscreen mode

你可以将这些修饰符用于属性、方法,甚至构造函数(但有一些限制)。务必记住,这些修饰符仅供 TypeScript 编译器和 IDE 使用,但由于 TypeScript 会被转译成 JavaScript,因此具有不同修饰符的成员之间没有任何区别。JavaScript 不提供任何更改类成员访问权限的选项,因此所有成员在输出的代码中都是公开可访问的。🤨

民众

如果没有直接指定修饰符,则使用此默认修饰符。它表示给定的成员可以公开访问,即在给定类的外部和内部都可以访问。

class MyClass {
    public myStringMember: string = 'str';

    public constructor() {
        this.myStringMember; // 'str'
    }
}

new MyClass().myStringMember; // 'str'
Enter fullscreen mode Exit fullscreen mode

它也是可以应用于构造函数的两个修饰符之一(默认情况下是)。公共构造函数允许在代码中的任何位置实例化你的类。

私人的

私有修饰符将类成员的访问权限限制在类内部。在类外部访问它会抛出错误。这遵循面向对象编程封装原则👏,允许你隐藏在给定作用域之外不需要的信息。

class MyClass {
    private myStringMember: string = 'str';

    constructor() {
        this.myStringMember; // 'str'
    }
}

new MyClass().myStringMember; // error
Enter fullscreen mode Exit fullscreen mode

总的来说,这项技术非常有用。可惜的是,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
Enter fullscreen mode Exit fullscreen mode

上面的代码片段应该能让你更好地理解发生了什么。注意,`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
    }
}
Enter fullscreen mode Exit fullscreen mode

readonly属性修饰符仅适用于属性(不适用于方法或构造函数),并且必须使用正确的关键字。此外,请记住,readonly 可以与其他辅助功能修饰符按特定顺序一起使用。

至于static修饰符,它的工作原理是使给定的成员在类本身而非其实例上可访问。此外,静态成员不能通过此修饰符访问,也不能被此修饰符访问。相反,您可以通过直接引用成员名称来访问类成员,例如 `this.static` MyClass。静态成员允许您定义跨实例的常量,或者将类用作各种方法的集合。

class MyClass {
    static myStringMember: string = 'str';

    constructor() {
        this.myStringMember // error
        MyClass.myStringMember // 'str'
    }

    static myMethod(): void {
        this; // error
    }
}
Enter fullscreen mode Exit fullscreen mode

抽象类

在文章前面,我提到了抽象类。那么,抽象类是什么呢?简单来说,抽象类就是不能被实例化的类,它们只能作为其他继承类的引用。至于语法,抽象类引入的唯一新特性就是abstract关键字 `__init__`。它用于定义类本身及其成员。

abstract class MyAbstractClass {
    abstract myAbstractMethod(): void;
    abstract myAbstractStringMember: string;

    constructor() {
        this.myMethod();
    }

    myMethod() {
        this.myAbstractMethod();
    }
}
Enter fullscreen mode Exit fullscreen mode

上面的例子展示了以(基本)正确的方式使用抽象类的全部潜力。我们已经知道 abstract 用于声明相应的类。但是,当 abstract 用于类成员时意味着什么呢?它表示被继承类需要自行实现的成员。如果找不到正确的实现,则会抛出错误。任何其他已实现的成员通常会被相应的类继承。🙂

class MyClass extends MyAbstractClass {
    myAbstractStringMember: string = 'str';
    myAbstractMethod(): void {
        // code
    };
}
new MyAbstractClass() // error
new MyClass().myAbstractStringMember; // 'str'
Enter fullscreen mode Exit fullscreen mode

申报时间

声明类时,实际上你做了两件事——创建给定类的实例类型和所谓的构造函数

创建实例类型允许您将变量的类型定义为特定类的实例。您可以像使用其他类型一样使用此类型,只需使用类名即可。

const instance: MyClass = new MyClass();
Enter fullscreen mode Exit fullscreen mode

另一方面,构造函数是在使用关键字创建给定类的实例时调用的函数new

但如果你想将构造函数本身而不是一个实例赋值给一个变量呢?在 JavaScript 中,你可以这样写:

const MyClassAlias = MyClass;
Enter fullscreen mode Exit fullscreen mode

classAlias但是,在 TypeScript 中,`type`的实际类型是什么呢?这就用到了typeof`type` 关键字,它之前我们只知道它是一种类型守卫。它允许你获取任何 JavaScript 值的类型,以便后续使用。所以,要回答这个问题:

const MyClassAlias: typeof MyClass = MyClass;
const instance: MyClass = new ClassAlias();
Enter fullscreen mode Exit fullscreen mode

最后,我们来看最后一个技巧。你多久会用构造函数参数来赋值类成员呢?这种情况非常常见,以至于 TypeScript 专门为此提供了一个快捷方式。你可以在参数前面加上任何可访问性或只读修饰符,这样你的参数就可以成为一个完整的类成员。是不是很有意思?😄

class MyClass {
    constructor(public myStringMember: string) {}

    myMethod(): void {
        this.myStringMember;
    }
}
Enter fullscreen mode Exit fullscreen mode

接口

现在我们已经充分了解了 TypeScript 类,是时候探索接口了!🎉 接口是许多静态类型语言的黄金标准。它们允许你定义和操作值的“形状”,而不是值本身。

接口通常用于描述复杂结构(例如对象和类)的形状。它们指示最终结构需要具有哪些公开可用的属性/成员。要定义一个接口,必须使用interface关键字和正确的语法:

interface MyInterface {
    readonly myStringProperty: string = 'str';
    myNumberProperty?: number;

    myMethodProperty(): void
}
Enter fullscreen mode Exit fullscreen mode

在接口声明中,我们可以使用之前学过的 TypeScript 语法,特别是只读属性可选属性默认值。接口还可以包含我们未来结构体需要实现的方法。

接口的主要用途之一是作为一种类型。您可以使用已知的语法来使用它。

const myValue: MyInterface = {
    myStringProperty: "str";
    myMethodProperty() {
        // code
    }
}
Enter fullscreen mode Exit fullscreen mode

接口还允许你描述诸如函数类构造函数之类的值。但是,它们各自的语法有所不同:

interface MyFunctionInterface {
    (myNumberArg: number, myStringArg: string): void;
}
Enter fullscreen mode Exit fullscreen mode
interface MyClassInterface {
    myStringMember: string;
}

interface MyClassConstructorInterface {
    new (myNumberArg: number): MyClassInterface;
}
Enter fullscreen mode Exit fullscreen mode

说到接口,你可以利用它们创建不同的类型,从而更好地展现JavaScript 的灵活性。这就是为什么你可以将上述接口与其他属性结合起来,创建所谓的混合类型。😉

interface MyHybridInterface {
    (myNumberArg: number, myStringArg: string): void;
    myNumberProperty: number;
    myStringProperty: string;
}
Enter fullscreen mode Exit fullscreen mode

例如,这个接口描述了一个具有两个附加属性的函数。这种模式可能不太常见,但在动态 JavaScript 中完全可行。

遗产

接口和类一样,可以相互扩展,也可以扩展类的属性!你可以使用简单的 `extends` 关键字语法,让你的接口扩展一个或多个接口(类不支持此功能)。在这种情况下,被扩展接口共享的属性会被合并成一个单独的属性。

interface MyCombinedInterface extends MyInterface, MyHybridInterface {
    myBooleanProperty: boolean;
}
Enter fullscreen mode Exit fullscreen mode

当接口继承一个类时,它会继承该类的所有成员,无论这些成员使用了什么访问修饰符。但是,访问修饰符的作用要到后面才会显现出来:当接口只能由提供私有成员的类或其派生类实现时,访问修饰符才会发挥作用。这是访问修饰符与接口交互的唯一情况。否则,接口本身仅描述值的格式,因此访问修饰符既不存在,也没有必要存在。🙂

interface MyCombinedInterface extends MyClass {
    myBooleanProperty: boolean;
}
Enter fullscreen mode Exit fullscreen mode

课程

接口和类之间有着特殊的联系。仅从它们的声明语法就能看出它们的相似之处。这是因为类可以实现接口。

class MyClass implements MyInterface {
    myStringProperty: string = 'str';
    myNumberProperty: number = 10;
}
Enter fullscreen mode Exit fullscreen mode

通过使用implements关键字,您可以指定给定类必须实现特定接口中描述的所有属性。这样,您就可以更快速地定义变量。

const myValue: MyInterface = new MyClass();
Enter fullscreen mode Exit fullscreen mode

还记得类构造函数接口吗?事情到这里就变得有点复杂了。我们之前讨论类的时候,我提到过,定义一个类时,你同时创建了实例类型(称为实例端)和构造函数(称为静态端)。使用类时,implements你实际上是在与实例端交互。你是在告诉编译器,该类的实例应该具有来自这个接口的属性。这就是为什么你不能这样写:

class MyClass implements MyClassConstructorInterface {
    // code
}
Enter fullscreen mode Exit fullscreen mode

这是因为这意味着该类的实例可以被自身实例化。正确的做法是,使用类构造函数接口来描述你需要的类,例如将其作为参数传递。或许一个完整的示例能更好地说明这一点。🚀

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);
Enter fullscreen mode Exit fullscreen mode

简要说明一下流程。首先,我们声明两个接口——一个用于实例端,定义实例结构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;
}
Enter fullscreen mode Exit fullscreen mode

枚举再探

在上一篇文章中,我们讨论了枚举如何为数值数据赋予更友好的名称。但不出所料,枚举的功能远不止于此。😃

除了数字之外,枚举还可以由字符串组成。在这种情况下,每个成员都必须被赋予一个固定的字符串值。所有其他与枚举相关的规则仍然适用。

enum MyStringEnum {
    A = 'str1',
    B = 'str2',
    C = 'str3',
}
Enter fullscreen mode Exit fullscreen mode

理论上,如果所有成员都直接赋值,那么你可以在枚举中自由混合使用字符串和数值。但实际上并没有太多应用场景。

枚举类型也可以在运行时作为类似对象的结构使用。此外,枚举成员不仅可以赋静态值,还可以计算值。因此,下面的赋值语句完全正确。

const myNumber: number = 20;

enum MyEnum {
    X = myNumber * 10,
    Y
};
const myObject: {X: number, Y: number} = MyEnum;
Enter fullscreen mode Exit fullscreen mode

编译后,枚举会以 JS 对象的形式存在。但是,如果您希望枚举仅作为常量值的集合,则可以使用 const 关键字轻松实现。

const enum MyEnum {
    X,
    Y
}
Enter fullscreen mode Exit fullscreen mode

在这样的常量枚举中,你不能像以前那样包含计算成员。这些枚举会在编译期间被移除,因此在它们被引用的地方只会留下常量值。

返回函数

我们已经对函数进行了很多讨论。但是,因为我们想了解更多,所以是时候看看一些更复杂的方面了。😉

默认值

与类成员类似,函数参数也可以设置默认值。函数可以有多个带有默认值的参数,但不能有任何未指定默认值的必需参数。只有当没有传递任何参数时,才会使用默认值。

function myFunc(myNumberArg: number, myDefaultStringArg: string = 'str') {
    // code
}
myFunc(10);
myFunc(10, 'string');
Enter fullscreen mode Exit fullscreen mode

.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'});
Enter fullscreen mode Exit fullscreen mode

通过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;
    }
}
Enter fullscreen mode Exit fullscreen mode

声明重载时,只需提供多个函数签名,然后使用更通用的类型(例如示例中的 `any`)定义实际函数。编译器随后会选择正确的重载并向 IDE 提供相应的信息。当然,同样的技术也可以用于类中。

其余参数

ES6 带来的另一个热门特性是剩余参数解构运算符。TypeScript 对这两个特性都提供了良好的支持。TypeScript 允许你像定义其他参数一样定义剩余参数的类型:

function myFunc(myNumberArg: number, ...myRestStringArg: string[]) {
    // code
}

myFunc(10, 'a', 'b', 'c');
Enter fullscreen mode Exit fullscreen mode

至于解构,TS 类型推断完全可以胜任。

悬念

哇,我们讲的内容真不少,是不是?有了类和接口,你现在就可以开始用 TypeScript 自己写一些面向对象编程 (OOP) 了。信不信由你,静态类型语言在运用 OOP 及其原则方面要好得多。不过,还有很多东西要讲。我们还没谈到泛型索引类型声明合并以及其他更复杂的内容。所以,请关注我的TwitterFacebook 主页,获取更多相关内容。另外,如果你喜欢这篇文章,请分享 一下,让更多人了解 TypeScript 和这个博客!😅 最后,别忘了在下方留言,说说你接下来想看到什么内容

就先这样吧……暂时就这些。👏

资源

现在你对 TypeScript 有了一些了解,是时候拓展你的知识了。去阅读、编写代码、学习吧,然后回来阅读第三部分!😉

文章来源:https://dev.to/areknawo/typescript-introduction-part-ii---classes--interfaces-5302