本文为「TypeScript 深入理解」系列第 2 篇,涵盖类、泛型、枚举、迭代器、继承、多态和抽象类。第 1 篇介绍了基础类型系统和函数签名。
如果说接口定义了数据的形状,那类和泛型解决的是更高层的问题——如何组织行为、如何让类型随数据流动。
类:TypeScript 的面向对象
TypeScript 为 ES6 的 class 提供了类型注解和访问修饰符,让你在面向对象编程时获得更强的约束。
访问修饰符控制成员的可见性:public(默认,外部可访问)、private(仅类内部可访问)、protected(类内部和子类可访问)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| class Person { public name: string; private age: number; protected gender: string; static species = 'Homo sapiens'; readonly birthDate: Date;
constructor(name: string, age: number, gender: string, birthDate: Date) { this.name = name; this.age = age; this.gender = gender; this.birthDate = birthDate; }
get formattedAge(): string { return `${this.age} years old`; }
set updateAge(newAge: number) { if (newAge < 0) throw new Error('Age cannot be negative'); this.age = newAge; } }
const person = new Person('Alice', 30, 'female', new Date('1994-01-01')); console.log(person.name); console.log(Person.species);
|
参数属性是一种简写方式,在构造函数参数前加上修饰符,TypeScript 会自动声明并赋值对应的属性:
1 2 3 4 5 6 7 8 9
| class Person { constructor( public name: string, private age: number, protected gender: string, readonly birthDate: Date ) {} }
|
泛型:让类型成为参数
泛型允许你编写可复用的代码,让类型成为参数。泛型本质上就是类型的参数化,你可以在函数、接口、类中使用它。
1 2 3 4 5 6 7
| function identity<T>(arg: T): T { return arg; }
console.log(identity<string>('hello')); console.log(identity(42));
|
泛型在集合操作中特别有用,比如一个返回数组最后一个元素的函数:
1 2 3 4 5 6 7
| function last<T>(arr: T[]): T | undefined { return arr[arr.length - 1]; }
console.log(last([1, 2, 3])); console.log(last(['a', 'b', 'c'])); console.log(last([]));
|
使用 extends 约束泛型参数的范围,确保传入的类型满足特定条件:
1 2 3 4 5 6 7 8
| function getLength<T extends { length: number }>(arg: T): number { return arg.length; }
console.log(getLength('hello')); console.log(getLength([1, 2, 3]));
|
泛型在接口和类中的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| interface KeyValuePair<K, V> { key: K; value: V; }
const pair1: KeyValuePair<string, number> = { key: 'age', value: 30 }; const pair2: KeyValuePair<number, string> = { key: 1, value: 'first' };
class Stack<T> { private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); } }
const numberStack = new Stack<number>(); numberStack.push(1); numberStack.push(2); console.log(numberStack.pop());
|
keyof 操作符常与泛型一起使用,用于约束属性名:
1 2 3 4 5 6 7
| function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; }
const user = { name: 'Alice', age: 30, email: 'alice@example.com' }; console.log(getProperty(user, 'name'));
|
枚举:给魔法数字一个名字
枚举为一组数值或字符串赋予友好的名称,让代码更具可读性。
1 2 3 4 5 6 7 8 9 10 11 12
| enum Direction { Up = 0, Down, Left, Right, }
console.log(Direction.Up); console.log(Direction.Right);
console.log(Direction[0]);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| enum HttpStatus { OK = '200 OK', NotFound = '404 Not Found', InternalError = '500 Internal Server Error', }
function handleResponse(status: HttpStatus) { switch (status) { case HttpStatus.OK: console.log('Request succeeded'); break; case HttpStatus.NotFound: console.log('Resource not found'); break; default: console.log('Something went wrong'); } }
|
1 2 3 4 5 6 7 8 9
| const enum Color { Red, Green, Blue, }
const c = Color.Red;
|
迭代器与生成器
TypeScript 支持 ES6 的迭代器协议和生成器语法。一个对象实现了 Symbol.iterator 方法就算可迭代的,for...of 循环会自动调用它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Range implements Iterable<number> { constructor(private start: number, private end: number) {}
[Symbol.iterator](): Iterator<number> { let current = this.start; const end = this.end; return { next(): IteratorResult<number> { if (current <= end) { return { value: current++, done: false }; } return { value: undefined, done: true }; }, }; } }
for (const n of new Range(1, 5)) { console.log(n); }
|
生成器用 function* 定义,通过 yield 逐步产出值,执行可以在每次 yield 后暂停:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function* fibonacci(): Generator<number, void, unknown> { let a = 0, b = 1; while (true) { yield a; [a, b] = [b, a + b]; } }
const fib = fibonacci(); console.log(fib.next().value); console.log(fib.next().value); console.log(fib.next().value); console.log(fib.next().value); console.log(fib.next().value);
|
for...of 遍历可迭代对象,for...in 遍历对象的可枚举属性(键名),两者不要混淆:
1 2 3 4 5 6 7 8 9
| const arr = ['a', 'b', 'c'];
for (const item of arr) { console.log(item); }
for (const key in arr) { console.log(key); }
|
面向对象:继承、多态与抽象
TypeScript 提供了完整的面向对象支持。如果你从 Java 或 C# 转过来,这些概念会很熟悉——但即便你一直是 JavaScript 开发者,理解它们也能让你的代码组织方式上一个台阶。
继承与多态
继承使用 extends 关键字,子类可以复用父类的属性和方法,也可以重写它们。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| class Animal { constructor(public name: string) {}
speak(): string { return `${this.name} makes a sound`; } }
class Dog extends Animal { speak(): string { return `${this.name} says Woof!`; } }
class Cat extends Animal { speak(): string { return `${this.name} says Meow!`; } }
const animals: Animal[] = [new Dog('Rex'), new Cat('Whiskers')]; animals.forEach(a => console.log(a.speak()));
|
方法重写时,子类可以通过 super 调用父类的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13
| class GuideDog extends Dog { constructor(name: string, public owner: string) { super(name); }
speak(): string { return `${super.speak()} (Guide dog for ${this.owner})`; } }
console.log(new GuideDog('Buddy', 'Alice').speak());
|
方法重载在类的上下文中同样适用。先声明重载签名,最后给出统一实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Calculator { calculate(x: number): number; calculate(x: string): number; calculate(x: number | string): number { if (typeof x === 'string') { return x.length; } return x * 2; } }
const calc = new Calculator(); console.log(calc.calculate(5)); console.log(calc.calculate('abc'));
|
抽象类:定义模板而非实现
abstract 修饰符用于定义抽象类和抽象方法。抽象类不能被直接实例化,只能被继承。抽象方法只有声明没有实现,子类必须提供具体实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| abstract class Shape { abstract getArea(): number;
describe(): string { return `This shape has an area of ${this.getArea()}`; } }
class Circle extends Shape { constructor(public radius: number) { super(); }
getArea(): number { return Math.PI * this.radius ** 2; } }
class Rectangle extends Shape { constructor(public width: number, public height: number) { super(); }
getArea(): number { return this.width * this.height; } }
const circle = new Circle(5); const rect = new Rectangle(4, 6);
console.log(circle.describe()); console.log(rect.describe());
|
抽象类和接口的核心区别:抽象类可以有实现代码(普通方法),接口只能定义签名。抽象类用来说明「是什么」的继承关系,接口用来声明「能做什么」的能力约定。
下一篇将介绍 TypeScript 的装饰器系统和编译原理——装饰器如何在编译期对类进行元编程改造,以及 tsc 从源码到 JS 的五步编译管线。