跳到主要内容

TypeScript进阶指南:从函数重载到高级泛型

阅读需 11 分钟
wqz

TypeScript作为JavaScript的超集,不仅提供了基本的类型系统,还有许多强大的进阶特性。本文将深入探讨TypeScript的进阶概念,帮助你编写更健壮、可维护的代码,充分发挥TypeScript的潜力。

1. 函数重载 - 一个函数,多种用法

函数重载允许一个函数根据不同的参数类型和数量,有不同的行为。这在API设计中特别有用,可以为调用者提供更清晰的类型提示。

// 函数重载声明
function hello(name: string): string;
function hello(age: number): string;

// 函数实现(必须兼容所有重载)
function hello(value: string | number): string {
if (typeof value === 'string') {
return `你的名字是 ${value}`;
} else if (typeof value === 'number') {
return `你的年龄是 ${value}`;
} else {
return '请输入正确的参数';
}
}

// 使用
hello('小明'); // 返回: "你的名字是 小明"
hello(18); // 返回: "你的年龄是 18"

重点说明

  • 先写所有的重载声明(没有函数体)
  • 最后写一个兼容所有重载的函数实现
  • 实现函数的参数类型通常是联合类型
  • TypeScript会根据传入的参数类型自动选择正确的重载
  • 重载签名必须与实现签名兼容

2. 接口与继承 - 定义对象的"规范"

基本接口

接口是TypeScript中定义对象结构的核心方式,它描述了对象应该具有的属性和方法。

interface Person {
name: string;
age: number;
sayHello(): void;
readonly id: number; // 只读属性
address?: string; // 可选属性
}

// 使用接口
const person: Person = {
name: '小明',
age: 18,
id: 1001,
sayHello() {
console.log(`你好,我是${this.name}`);
}
};

接口继承

接口可以继承其他接口,获得父接口的所有属性和方法。

interface Person {
name: string;
age: number;
}

interface Student extends Person {
school: string;
grade: number;
}

// 使用继承的接口
const student: Student = {
name: '小明',
age: 18,
school: '清华大学',
grade: 1
};

接口合并

同名接口会自动合并,这是TypeScript的一个强大特性。

interface Box {
height: number;
width: number;
}

interface Box {
length: number;
color: string;
}

// 最终的Box接口包含所有属性
const box: Box = {
height: 10,
width: 20,
length: 30,
color: 'red'
};

3. 命名空间 - 组织和管理代码

命名空间用于将相关的代码组织在一起,避免命名冲突。

namespace BlogModule {
export interface Article {
title: string;
content: string;
}

export class BlogPost implements Article {
constructor(
public title: string,
public content: string,
public author: string
) {}

publish() {
console.log(`发布文章: ${this.title}`);
}
}

// 非导出成员只能在命名空间内部访问
function formatTitle(title: string): string {
return `${title}`;
}
}

// 使用命名空间中的类
const article = new BlogModule.BlogPost('TypeScript教程', '内容...', '小明');
article.publish();

重点说明

  • 使用namespace关键字创建命名空间
  • 使用export关键字导出需要在外部访问的成员
  • 命名空间可以嵌套
  • 命名空间是TypeScript特有的,在现代Web开发中,更推荐使用ES模块

4. 类与访问修饰符 - 控制成员的可见性

TypeScript提供了三种访问修饰符,用于控制类成员的可见性。

class Article {
public title: string; // 公开的,任何地方都可以访问
content: string; // 默认为public
private views: number; // 私有的,只能在类内部访问
protected status: string; // 受保护的,只能在类内部和子类中访问
readonly id: number; // 只读属性,初始化后不能修改

constructor(title: string, content: string) {
this.title = title;
this.content = content;
this.views = 0;
this.status = 'draft';
this.id = Math.random();
}

private updateViews() {
this.views++;
}

public read() {
this.updateViews();
return this.content;
}
}

const article = new Article('标题', '内容');
console.log(article.title); // 正确:public成员
// console.log(article.views); // 错误:private成员
// console.log(article.status); // 错误:protected成员
// article.id = 100; // 错误:readonly属性

访问修饰符说明

  • public:默认值,可以在任何地方访问
  • private:只能在类内部访问
  • protected:只能在类内部和子类中访问
  • readonly:只读属性,可以与其他修饰符组合使用

5. 静态成员 - 属于类而非实例

静态成员属于类本身,而不是类的实例。

class Article {
title: string;
content: string;

static count: number = 0; // 静态属性
private static readonly VERSION = '1.0'; // 私有只读静态属性

constructor(title: string, content: string) {
this.title = title;
this.content = content;
Article.count++; // 增加文章计数
}

static getInfo() {
return `文章系统 v${Article.VERSION},当前共有${Article.count}篇文章`;
}
}

const article1 = new Article('标题1', '内容1');
const article2 = new Article('标题2', '内容2');

console.log(Article.count); // 输出: 2
console.log(Article.getInfo()); // 输出: "文章系统 v1.0,当前共有2篇文章"
// console.log(article1.count); // 错误:静态成员只能通过类访问

重点说明

  • 使用static关键字定义静态成员
  • 静态成员通过类名访问,而不是实例
  • 静态成员可以与访问修饰符和readonly组合使用
  • 静态成员适合存储与类相关但不依赖于特定实例的数据和功能

6. 构造函数参数属性 - 简化代码

TypeScript允许在构造函数参数上使用访问修饰符,自动创建并初始化同名的类属性。

// 传统方式
class Article {
title: string;
content: string;
private author: string;

constructor(title: string, content: string, author: string) {
this.title = title;
this.content = content;
this.author = author;
}
}

// 使用参数属性简化
class ArticleSimple {
constructor(
public title: string,
public content: string,
private author: string,
readonly id: number = Math.random()
) {
// 不需要额外的赋值代码
}
}

重点说明

  • 在构造函数参数前添加访问修饰符,自动创建并初始化同名属性
  • 可以大大减少样板代码
  • 可以与普通参数混用
  • 可以与readonly修饰符组合使用
  • 可以提供默认值

7. 类继承 - 扩展现有类

类可以通过继承获得父类的属性和方法。

class Article {
constructor(
public title: string,
public content: string,
protected author: string
) {}

getInfo() {
return `${this.title}》 by ${this.author}`;
}
}

class BlogPost extends Article {
constructor(
title: string,
content: string,
author: string,
public tags: string[]
) {
super(title, content, author); // 调用父类构造函数
}

// 重写父类方法
getInfo() {
return `${super.getInfo()} [${this.tags.join(', ')}]`;
}

// 添加新方法
getAuthorInfo() {
return `作者: ${this.author}`; // 可以访问protected成员
}
}

const post = new BlogPost('TypeScript教程', '内容...', '小明', ['TypeScript', '教程']);
console.log(post.getInfo()); // 输出: "《TypeScript教程》 by 小明 [TypeScript, 教程]"

重点说明

  • 使用extends关键字实现继承
  • 子类构造函数必须调用super()
  • super()必须在访问this之前调用
  • 子类可以重写父类的方法,使用super.方法名()调用父类方法
  • 子类可以访问父类的publicprotected成员,但不能访问private成员

8. Getter和Setter - 控制属性的访问

Getter和Setter允许你控制属性的读取和设置行为。

class User {
private _password: string = '';
private _email: string = '';

// 读取属性时调用
get password(): string {
return '*****'; // 不返回实际密码,而是星号
}

// 设置属性时调用
set password(newPass: string) {
if (newPass.length < 6) {
throw new Error('密码长度不能小于6位');
}
this._password = newPass;
}

get email(): string {
return this._email;
}

set email(value: string) {
if (!value.includes('@')) {
throw new Error('邮箱格式不正确');
}
this._email = value;
}
}

const user = new User();
user.password = '123456'; // 调用setter
console.log(user.password); // 调用getter,输出: *****

try {
user.password = '123'; // 抛出错误:密码长度不能小于6位
} catch (e) {
console.error(e.message);
}

重点说明

  • 使用getset关键字定义访问器
  • Getter不能有参数,Setter必须有一个参数
  • 可以用来验证数据、转换值或保护私有属性
  • 使用时像普通属性一样访问,不需要调用函数
  • 可以只定义getter(只读属性)或只定义setter(只写属性)

9. 抽象类 - 定义"不完整"的类

抽象类是不能直接实例化的类,用于定义子类应该实现的结构。

abstract class Animal {
abstract name: string; // 抽象属性,子类必须实现
abstract makeSound(): void; // 抽象方法,子类必须实现

move(): void { // 普通方法,子类可以直接使用
console.log(`${this.name}正在移动`);
}
}

class Cat extends Animal {
name: string = '小猫'; // 实现抽象属性

makeSound(): void { // 实现抽象方法
console.log('喵喵喵');
}

// 可以添加自己的方法
scratch(): void {
console.log('猫抓了一下');
}
}

// const animal = new Animal(); // 错误:不能创建抽象类的实例
const cat = new Cat();
cat.makeSound(); // 输出: 喵喵喵
cat.move(); // 输出: 小猫正在移动

重点说明

  • 使用abstract关键字定义抽象类和抽象成员
  • 抽象类不能被直接实例化
  • 抽象成员必须在子类中实现
  • 抽象类可以包含已实现的方法和属性
  • 抽象类适合定义通用的基类结构

10. 接口实现 - 类的"合同"

类可以实现一个或多个接口,保证类具有接口定义的所有成员。

interface Animal {
name: string;
makeSound(): void;
}

interface Pet {
owner: string;
play(): void;
}

// 实现多个接口
class Dog implements Animal, Pet {
name: string = '小狗';
owner: string = '小明';

makeSound(): void {
console.log('汪汪汪');
}

play(): void {
console.log(`${this.name}${this.owner}一起玩`);
}

// 可以添加接口中未定义的方法
fetch(): void {
console.log('小狗去捡球了');
}
}

const dog = new Dog();
dog.makeSound(); // 输出: 汪汪汪
dog.play(); // 输出: 小狗和小明一起玩

重点说明

  • 使用implements关键字实现接口
  • 类必须实现接口中定义的所有成员
  • 一个类可以实现多个接口
  • 接口只定义结构,不提供实现
  • 类可以添加接口中未定义的额外成员

11. 泛型 - 可重用的类型安全代码

泛型允许你创建可以处理多种类型的组件,同时保持类型安全。

// 泛型类
class Box<T> {
private value: T;

constructor(value: T) {
this.value = value;
}

getValue(): T {
return this.value;
}

setValue(value: T): void {
this.value = value;
}
}

// 使用字符串类型实例化
const stringBox = new Box<string>('Hello');
const greeting: string = stringBox.getValue(); // 类型安全
// stringBox.setValue(42); // 错误:类型不匹配

// 使用数字类型实例化
const numberBox = new Box<number>(100);
const value: number = numberBox.getValue(); // 类型安全
// numberBox.setValue('abc'); // 错误:类型不匹配

// 泛型函数
function identity<T>(arg: T): T {
return arg;
}

const num = identity<number>(42); // 明确指定类型
const str = identity('hello'); // 类型推断为string

重点说明

  • 使用尖括号<T>定义泛型参数
  • T是类型变量,可以是任何名称
  • 泛型可以用于类、接口、函数和类型别名
  • 实例化时可以明确指定类型,也可以让TypeScript自动推断
  • 泛型保证了类型安全,防止类型错误

12. 高级泛型技巧

泛型约束

限制泛型参数必须具有特定的结构。

interface HasLength {
length: number;
}

// T必须有length属性
function printLength<T extends HasLength>(value: T): void {
console.log(value.length);
}

printLength('hello'); // 正确:字符串有length属性
printLength([1, 2, 3]); // 正确:数组有length属性
// printLength(123); // 错误:数字没有length属性

多个泛型参数

// 键值对函数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}

const result = pair<string, number>('age', 30);
console.log(result); // ['age', 30]

// 映射类型
interface Person {
name: string;
age: number;
}

// 将所有属性变为可选
type PartialPerson = Partial<Person>;
// 等同于: { name?: string; age?: number; }

// 将所有属性变为只读
type ReadonlyPerson = Readonly<Person>;
// 等同于: { readonly name: string; readonly age: number; }

泛型默认值

interface Config<T = string> {
value: T;
defaultValue: T;
}

const stringConfig: Config = {
value: 'hello',
defaultValue: ''
};

const numberConfig: Config<number> = {
value: 42,
defaultValue: 0
};

条件类型

// 条件类型:如果T是U的子类型,则为X,否则为Y
type TypeName<T> = T extends string ? 'string' :
T extends number ? 'number' :
T extends boolean ? 'boolean' :
T extends undefined ? 'undefined' :
T extends Function ? 'function' :
'object';

type T0 = TypeName<string>; // 'string'
type T1 = TypeName<number>; // 'number'
type T2 = TypeName<() => void>; // 'function'

总结

TypeScript的进阶特性让你能够编写更加健壮、可维护的代码:

  1. 函数重载:根据不同参数提供不同实现
  2. 接口与继承:定义对象结构并支持继承
  3. 命名空间:组织和管理相关代码
  4. 类与访问修饰符:控制成员的可见性
  5. 静态成员:定义属于类而非实例的成员
  6. 构造函数参数属性:简化类的初始化代码
  7. 类继承:扩展现有类的功能
  8. Getter和Setter:控制属性的访问
  9. 抽象类:定义子类必须实现的结构
  10. 接口实现:确保类符合特定的结构
  11. 泛型:创建类型安全且可重用的组件

掌握这些进阶特性,将帮助你充分利用TypeScript的类型系统,编写更加健壮、可维护的代码。随着你的深入学习,你会发现TypeScript的类型系统非常强大,能够表达复杂的设计模式和架构。

参考资源

分享这篇文章
Loading Comments...