十个必须掌握的TypeScript核心技巧与最佳实践

作者:半夏

时间:2026-10-30

浏览:145

本文深入解析TypeScript的十个核心技巧,涵盖泛型在数组反转中的应用、交叉与联合类型的组合使用、类型推断机制、keyof与映射类型的操作,以及类型别名、接口、装饰器、类型守卫、声明文件编写和类型化事件的处理。通过具体代码示例,帮助开发者提升代码的类型安全性、可维护性与复用性,适用于前端工程化与大型项目构建场景。

文章目录

1. 泛型的使用

泛型机制允许开发者编写具备更高灵活性、复用性及类型安全性的代码。在 TypeScript 中,泛型通过类型参数来定义通用的类型或函数,并在实际调用时指定具体的类型参数。

假设我们需要编写一个函数来反转任意类型的数组,若不使用泛型,代码实现可能如下所示:

function reverseStrings(items: string[]): string[] {
  return items.reverse();
}
function reverseNumbers(items: number[]): number[] {
  return items.reverse();
}

然而,这种方式显然缺乏优雅性。我们需要分别为 string 和 number 类型的数组编写独立的函数,且当未来需要处理其他类型时,必须重复编写新的函数,导致代码冗余。

借助泛型,我们可以轻松创建一个通用函数,从而处理任意类型的数组:

function reverse<T>(items: T[]): T[] {
  return items.reverse();
}
const words = ['hello', 'world'];
const reversedWords = reverse<string>(words); 
console.log(reversedWords); // ['world', 'hello']
const numbers = [1, 2, 3];
const reversedNumbers = reverse<number>(numbers);
console.log(reversedNumbers); // [3, 2, 1]

2. 利用交叉类型和联合类型

交叉类型(Intersection Types)支持将多个类型合并为单一类型,新类型将继承所有被合并类型的特性。我们可以使用 & 运算符将两个或多个类型组合成一个交叉类型。

联合类型(Union Types)表示一个值可以是多种类型中的一种。我们可以使用 | 运算符将两个或多个类型组合成一个联合类型。

// 交叉类型
interface Dog {
  walk(): void;
}
interface Cat {
  meow(): void;
}
type Pet = Dog & Cat;
const myPet: Pet = {
  walk() { console.log('walking') },
  meow() { console.log('meowing') }
}

联合类型的用法十分直观,只需使用 | 运算符将多个类型组合在一起即可。

interface Square {
  side: number;
}
interface Circle {
  radius: number;
}
function calculateArea(shape: Square | Circle) {
  if ('side' in shape) {
    return shape.side ** 2;
  } else {
    return Math.PI * shape.radius ** 2;
  }
}

3. 类型推断

类型推断(Type Inference)是 TypeScript 的一项强大特性。它允许编译器根据上下文自动推断变量的类型,从而减少手动指定类型的工作量,同时提升了代码的可维护性与可读性。

示例如下:

let num = 5;
let str = "hello";
let bool = true;
function add(a: number, b: number) {
  return a + b;
}
let result = add(num, 10);

4. keyof

keyof 是 TypeScript 中的一个关键字,用于获取对象类型中所有键名组成的联合类型。它在编写泛型函数或操作对象属性时,能提供更为严格的类型安全保障。

interface Person {
  name: string;
  age: number;
  gender: 'male' | 'female';
}
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
let person: Person = { name: 'Alice', age: 30, gender: 'female' };
let name = getProperty(person, 'name');
let age = getProperty(person, 'age');
let gender = getProperty(person, 'gender');

5. 映射类型

TypeScript 中的映射类型(Mapped Types)是一种强大的类型操作符,它基于已有的对象类型生成新的对象类型。映射类型有助于执行常见的类型转换与操作,例如将所有属性变为可选、添加或删除属性、修改属性类型等。

TypeScript 中主要包含以下四种映射类型:

1. Partial<T>:将类型 T 中的所有属性变为可选属性。

interface Person {
  name: string;
  age: number;
  gender: 'male' | 'female';
}
type PartialPerson = Partial<Person>;
// 等价于
// interface PartialPerson {
//   name?: string;
//   age?: number;
//   gender?: 'male' | 'female';
// }

2. Readonly<T>:将类型 T 中的所有属性变为只读属性。

interface Person {
  name: string;
  age: number;
  gender: 'male' | 'female';
}
type ReadonlyPerson = Readonly<Person>;
// 等价于
// interface ReadonlyPerson {
//   readonly name: string;
//   readonly age: number;
//   readonly gender: 'male' | 'female';
// }

3. Record<K, T>:创建一个新的对象类型,其属性名类型为 K,属性值类型为 T。

type Dictionary<T> = Record<string, T>;
let dict: Dictionary<number> = {
  foo: 123,
  bar: 456,
};

4. Pick<T, K>:从类型 T 中选择指定的属性 K,并返回一个新的对象类型。

interface Person {
  name: string;
  age: number;
  gender: 'male' | 'female';
}
type PersonNameAndAge = Pick<Person, 'name' | 'age'>;
// 等价于
// interface PersonNameAndAge {
//   name: string;
//   age: number;
// }

此外还有一种映射类型名为 keyof,它用于获取对象类型中所有属性名组成的联合类型。该类型在前文已作介绍,此处不再赘述。

6. 类型别名和接口

1. 类型别名

类型别名(Type Aliases)是一种为已有类型赋予新名称的方式。通过 type 关键字可以定义一个类型别名。

type MyString = string;
type MyNumber = number;
type Person = {
  name: string;
  age: number;
};

类型别名能够方便地为复杂类型定义简洁的名称,从而提高代码可读性,并且还可以结合联合类型、交叉类型等高级类型特性使用。

type Color = 'red' | 'green' | 'blue';
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; length: number };
function draw(shape: Shape, color: Color) {
  // ...
}

2. 接口

接口(Interfaces)是一种描述对象结构的方式,在 TypeScript 中通过 interface 关键字来定义。接口可以包含属性、方法和索引签名等成员。

interface Person {
  name: string;
  age: number;
  sayHello: () => void;
}
let person: Person = {
  name: 'Alice',
  age: 30,
  sayHello() {
    console.log(`Hello, my name is ${this.name}.`);
  },
};

接口在描述对象结构时非常有用,它能提供更好的代码组织性和可读性,并且在某些特定场景下能提供更强的类型安全性。另外需要注意的是,接口仅能描述对象的形状,无法描述具体的实现方式。若需描述具体实现,可以使用类或函数类型。

7. 装饰器

装饰器是一种特殊的语法结构,用于修饰类、方法、属性以及参数等元素,以实现特定目的。在 TypeScript 中,我们可以使用 @ 符号来声明一个装饰器。

function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;
  descriptor.value = function(...args: any[]) {
    console.log(`Call ${key} with args: ${JSON.stringify(args)}`);
    return originalMethod.apply(this, args);
  };
  return descriptor;
}
class MyClass {
  @log
  greet(name: string) {
    console.log(`Hello, ${name}!`);
  }
}

TypeScript 中的装饰器适用于多种场景,例如实现依赖注入、自动绑定事件、路由映射等。常见的装饰器包括 @Injectable、@Component、@ViewChild、@RouterConfig 等。

8. 类型守卫

类型守卫(Type Guards)是 TypeScript 中用于检测类型的机制,它帮助开发者在运行时检测变量的类型,并在不同条件下提供相应的类型声明。

在 TypeScript 中,常见的类型守卫方式有四种:

1. typeof 类型守卫

function foo(x: number | string) {
  if (typeof x === 'number') {
    // x is number
  } else {
    // x is string
  }
}

2. instanceof 类型守卫

class MyClass {}
function foo(x: any) {
  if (x instanceof MyClass) {
    // x is an instance of MyClass
  }
}

3. 自定义类型守卫函数

interface A { a: number }
interface B { b: number }
function isA(x: any): x is A {
  return typeof x.a === 'number';
}
function foo(x: A | B) {
  if (isA(x)) {
    // x is an instance of A
  } else {
    // x is an instance of B
  }
}

4. in 操作符类型守卫

interface A { a: number }
interface B { b: number }
function foo(x: A | B) {
  if ('a' in x) {
    // x is an instance of A
  } else {
    // x is an instance of B
  }
}

9. 声明文件

声明文件(Declaration File)是一种特殊的类型文件,用于描述外部 JavaScript 库、模块或对象的类型,以便在 TypeScript 代码中正确引用和使用它们。

TypeScript 编译器可以根据 JavaScript 库的源代码推断其类型信息,但某些 JavaScript 库并未提供类型定义文件,或者类型定义文件不完整、不准确,此时我们需要手动编写声明文件。声明文件的扩展名为 .d.ts,可与 TypeScript 文件一同放置在项目目录中。声明文件的编写方式主要有以下几种:

1. 定义全局变量和函数

若需在 TypeScript 代码中调用浏览器原生 API 或其他 JavaScript 库中的全局变量和函数,就需要手动编写声明文件来告知 TypeScript 对应变量和函数的类型。例如:

declare const $: (selector: string) => any;
$('#my-element').addClass('highlight');

2. 扩展已有类型

有时我们需要扩展已有的类型定义以适应自身需求,此时可以使用 interface、namespace 等关键字来定义和扩展类型。例如:

interface String {
  reverse(): string;
}
const str = 'Hello, world!';
console.log(str.reverse()); // "!dlrow ,olleH"

3. 模块声明

若我们要使用一个已有的 JavaScript 模块,但模块本身未提供类型定义文件,或者类型定义文件不完整、不准确,此时我们需要手动编写声明文件来告知 TypeScript 模块的类型信息。例如:

declare module 'my-module' {
  export function greet(name: string): string;
}

10. 类型化事件

类型化事件(Typed Event)是一种可以指定事件处理函数接收参数类型及返回值类型的事件机制。通过使用类型化事件,我们可以在编译阶段对事件处理函数的类型进行检查,从而避免运行时因类型不匹配导致的错误。

示例如下,展示如何定义和使用类型化事件:

interface EventHandler<T> {
  (args: T): void;
}
class TypedEvent<T> {
  private handlers: EventHandler<T>[] = [];
  public addHandler(handler: EventHandler<T>) {
    this.handlers.push(handler);
  }
  public removeHandler(handler: EventHandler<T>) {
    const index = this.handlers.indexOf(handler);
    if (index >= 0) {
      this.handlers.splice(index, 1);
    }
  }
  public raise(args: T) {
    for (const handler of this.handlers) {
      handler(args);
    }
  }
}
// 定义一个事件参数类型
interface MyEventArgs {
  message: string;
}
// 创建一个类型化事件实例
const myEvent = new TypedEvent<MyEventArgs>();
// 添加一个事件处理函数
myEvent.addHandler((args: MyEventArgs) => {
  console.log(args.message);
});
// 触发事件
myEvent.raise({ message: 'Hello, world!' });

以上便是十个必须掌握的 TypeScript 技巧的详细内容,更多关于 TypeScript 技巧的资料请持续关注本站其他相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章