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 中的映射类型有以下四种:
-
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';
// }
-
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';
// }
-
Record<K, T>
:创建一个新的对象类型,其属性名类型为K
,属性值类型为T
。
type Dictionary<T> = Record<string, T>;
let dict: Dictionary<number> = {
foo: 123,
bar: 456,
};
-
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 中,有四种常见的类型守卫方式:
-
typeof
类型守卫
function foo(x: number | string) {
if (typeof x === 'number') {
// x is number
} else {
// x is string
}
}
-
instanceof
类型守卫
class MyClass {}
function foo(x: any) {
if (x instanceof MyClass) {
// x is an instance of MyClass
}
}
- 自定义类型守卫函数
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
}
}
-
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 文件一起放置在项目目录中。声明文件的编写方式有以下几种:
- 定义全局变量和函数
如果我们需要在 TypeScript 代码中调用浏览器原生 API 或其他 JavaScript 库中的全局变量和函数,就需要手动编写声明文件来告诉 TypeScript 对应变量和函数的类型。例如:
declare const $: (selector: string) => any;
$('#my-element').addClass('highlight');
- 扩展已有类型
有时候我们需要扩展已有的类型定义,以适应自己的需求,这时可以使用 interface
、namespace
等关键字来定义和扩展类型。例如:
interface String {
reverse(): string;
}
const str = 'Hello, world!';
console.log(str.reverse()); // "!dlrow ,olleH"
- 模块声明
如果我们要使用一个已有的 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!' });