高级类型

89 阅读9分钟

交叉类型(Intersection Types)

交叉类型(Intersection Types)是一种强大的特性,它允许你将多个类型合并为一个类型。这意呀着,你可以将多个接口(Interface)、类型别名(Type Aliases)或者字面量类型(Literal Types)组合起来,形成一个新的类型,这个新类型将包含所有组合类型的成员。

交叉类型的语法

交叉类型的语法非常简单,你只需要使用&符号来合并多个类型。

type TypeA = { a: string };  
type TypeB = { b: number };  
type TypeC = TypeA & TypeB; // 交叉类型  
  
const obj: TypeC = {  
  a: "hello",  
  b: 123,  
};

在上面的例子中,TypeC是一个交叉类型,它同时包含了TypeA和TypeB的所有属性。因此,obj必须同时满足TypeA和TypeB的要求,即同时拥有a和b属性。

交叉类型的应用场景

交叉类型在TypeScript中非常有用,尤其是在以下场景中:

  1. 合并现有类型:当你需要在一个对象上添加新的属性,但又不想修改原有类型定义时,交叉类型可以帮助你轻松地合并现有类型和新的属性。
  2. 结合函数签名:你可以使用交叉类型来结合多个函数签名,这在处理重载函数时特别有用。
  3. 混合类型:在某些情况下,你可能需要将类或接口的实例与其他类型混合(比如,在对象上添加一些静态属性或方法)。交叉类型允许你这样做,尽管在实践中,这种情况较少见,因为TypeScript的类不支持与接口或类型别名的直接交叉。

注意事项

  • 交叉类型不会自动合并具有相同名称但类型不兼容的属性。如果两个类型具有相同名称的属性,但类型不同,TypeScript将报错,因为它不知道应如何合并这些属性。
  • 交叉类型可以非常灵活地组合多个类型,但过度使用可能会导致类型定义变得复杂和难以理解。因此,建议只在必要时使用交叉类型,并尽量保持类型定义的简洁性。

联合类型(Union Types)

联合类型(Union Types)是一种非常有用的特性,它允许你将多个类型组合成一个类型。这个组合类型可以是这些类型中的任何一个,但一次只能是一个。联合类型通过竖线(|)分隔每个类型来定义。

使用联合类型

联合类型在处理不确定的类型时特别有用,比如你可能从一个函数返回一个字符串或者一个数字,或者你可能在处理来自外部源(如API)的数据,这些数据可能具有多种不同的格式。

// 定义一个联合类型,它可以是string或number  
type StringOrNumber = string | number;  
  
// 使用这个联合类型  
function processValue(value: StringOrNumber) {  
  if (typeof value === 'string') {  
    console.log(`The value is a string: ${value}`);  
  } else {  
    console.log(`The value is a number: ${value}`);  
  }  
}  
  
processValue('Hello, world!'); // 输出: The value is a string: Hello, world!  
processValue(42); // 输出: The value is a number: 42

访问联合类型的属性

当你尝试访问联合类型对象的属性时,TypeScript会强制你进行额外的检查,以确保你访问的属性在该类型的所有成员上都存在。这是因为TypeScript编译器需要保证类型安全,防止你在运行时遇到不存在的属性。

interface Bird {  
  fly(): void;  
  layEggs(): void;  
}  
  
interface Fish {  
  swim(): void;  
  layEggs(): void;  
}  
  
type Animal = Bird | Fish;  
  
function getAnimalActivity(animal: Animal): void {  
  if ((animal as Bird).fly) {  
    (animal as Bird).fly();  
  } else {  
    (animal as Fish).swim();  
  }  
    
  // 总是可以调用 layEggs,因为它是所有动物的共同方法  
  animal.layEggs();  
}  
  
// 注意:上面的类型断言(animal as Bird 和 animal as Fish)在实际应用中可能不是最佳实践,  
// 因为它们可能不是类型安全的。更好的做法是使用类型守卫(Type Guards)或`in`操作符来检查属性。  
  
// 使用类型守卫的改进示例:  
function isFish(animal: Animal): animal is Fish {  
  return (animal as Fish).swim !== undefined;  
}  
  
function getAnimalActivitySafe(animal: Animal): void {  
  if (isFish(animal)) {  
    animal.swim();  
  } else {  
    (animal as Bird).fly();  
  }  
  animal.layEggs();  
}

注意事项

  • 当处理联合类型时,确保你访问的属性或方法在所有可能的类型中都存在,否则TypeScript编译器会报错。
  • 使用类型守卫(如typeof操作符、instanceof关键字、自定义函数等)来安全地检查联合类型的实际类型。
  • 尽量避免过度依赖类型断言(如animal as Bird),因为它们会绕过TypeScript的类型检查系统,增加运行时错误的风险。

映射类型(Mapped Types)

映射类型允许通过旧类型的属性来创建新类型。它通常与泛型一起使用,用于批量更改或添加属性。

type Options = { readonly id: number; title?: string; };  
type MutableOptions = {  
  -readonly [P in keyof Options]: Options[P];  
}; // MutableOptions 移除了 id 属性的只读修饰符,但保留了其他属性

条件类型(Conditional Types)

条件类型允许根据条件选择不同的类型。它常用于泛型和复杂类型逻辑。

type IsString<T> = T extends string ? true : false;  
type A = IsString<string>; // true  
type B = IsString<number>; // false

可辨识联合(Discriminated Unions)

可辨识联合是一种高级模式,用于处理具有共同字段以区分不同类型的数据。

interface Circle { kind: "circle"; radius: number; }  
interface Square { kind: "square"; sideLength: number; }  
type Shape = Circle | Square;  
  
function getArea(shape: Shape) {  
  if (shape.kind === "circle") {  
    return Math.PI * shape.radius ** 2;  
  } else {  
    return shape.sideLength ** 2;  
  }  
}

泛型(Generics)

泛型允许定义灵活、可重用的组件,这些组件可以处理多种类型的数据。泛型通常用TUV等作为类型参数。

function showType<T>(args: T) {  
  console.log(args);  
}  
showType('test'); // Output: "test"  
showType(1); // Output: 1

实用工具类型(Utility Types)

TypeScript 提供了许多内置的实用工具类型,如PartialRequiredReadonlyPickOmitExtractExclude等,这些类型可以方便地修改或选择类型属性。

interface Person {  
  name: string;  
  age: number;  
}  
  
type PersonPartial = Partial<Person>; // 所有属性变为可选  
type PersonRequired = Required<Person>; // 所有属性变为必选  
type PersonReadonly = Readonly<Person>; // 所有属性变为只读

字符串字面量类型(String Literal Types)

字符串字面量类型允许你指定字符串的固定值。

type Greeting = "Hello" | "Hi";

数字字面量类型(Number Literal Types)

数字字面量类型允许你指定数字的固定值。

function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {  
  // ...  
}

枚举成员类型(Enum Member Types)

枚举类型允许你定义一组命名的常量。枚举成员也具有类型。

enum Direction {  
  Up,  
  Down,  
  Left,  
  Right  
}  
  
function move(direction: Direction) {  
  // ...  
}

索引类型(Indexed Types)

索引类型用于描述对象索引的类型。

索引签名可以是只读的或可写的,并且可以指定键的类型和值的类型。

// 使用字符串索引签名  
interface StringIndexed {  
  [key: string]: any; // 表示这个对象可以有任意数量的字符串属性,这些属性的值可以是任意类型  
}  
  
let obj: StringIndexed = {  
  a: 1,  
  b: 'hello',  
  c: true  
};  
  
// 使用数字索引签名  
interface ArrayLike<T> {  
  [index: number]: T; // 表示这个对象可以有任意数量的数字属性(索引),这些属性的值都是T类型  
  length?: number; // 长度属性是可选的,但如果你想要模拟数组,它通常会存在  
}  
  
let arrLike: ArrayLike<number> = {  
  0: 1,  
  1: 2,  
  2: 3,  
  length: 3  
};  
  
// 索引签名也可以与其他属性共存  
interface Counter {  
  [index: string]: number; // 任意字符串索引都映射到数字  
  total: number; // 但是,total不是通过索引访问的  
}  
  
let c: Counter = {  
  foo: 1,  
  bar: 2,  
  total: 3,  
  baz: 4 // 这里baz也是有效的,因为它符合索引签名  
};  
  
// 索引签名可以是只读的  
interface ReadonlyStringMap {  
  readonly [key: string]: string; // 所有通过字符串索引访问的属性都是只读的  
}  
  
let myMap: ReadonlyStringMap = {  
  key1: 'value1',  
  key2: 'value2'  
};  
  
// 尝试修改索引属性的值将会导致编译错误  
// myMap['key1'] = 'otherValue'; // 错误

类型保护与区分类型(Type Guards and Differentiating Types)

类型保护(Type Guards)和区分类型(Differentiating Types)是处理联合类型时非常重要的概念。类型保护是一种表达式,它执行运行时检查以确保某个值属于某个特定类型。这允许你在不使用类型断言(这可能会绕过TypeScript的类型检查)的情况下安全地访问该类型的属性或方法。

类型保护的类型

TypeScript提供了几种类型保护的形式:

  1. typeof 类型保护
    使用JavaScript的typeof操作符来区分基本类型。
function isString(x: any): x is string {  
  return typeof x === "string";  
}
  1. instanceof 类型保护
    使用instanceof来检查一个对象是否是某个类的实例。
class MyClass {}  
function isMyClassInstance(x: any): x is MyClass {  
  return x instanceof MyClass;  
}
  1. 自定义类型保护
    你可以通过返回类型为x is T的函数来创建自定义类型保护,其中T是你想要保护的类型。
interface Cat {  
  name: string;  
  purr(): void;  
}  

interface Dog {  
  name: string;  
  bark(): void;  
}  

function isCat(a: Cat | Dog): a is Cat {  
  return (a as Cat).purr !== undefined;  
}  

// 注意:上面的实现可能不是类型安全的,因为它依赖于`purr`方法的存在。  
// 更好的做法是使用一个明确的方法或属性来区分类型。  

function isCatSafe(a: Cat | Dog): a is Cat {  
  return (a as Cat).purr !== undefined && typeof (a as Cat).purr === 'function';  
}
  1. in 操作符类型保护
    使用in操作符来检查对象是否具有某个属性。虽然它本身不直接作为类型保护,但可以与typeof或自定义类型保护结合使用来区分类型。
function hasColor(x: any): x is { color: string } {  
  return 'color' in x;  
}

区分类型

区分类型主要是指在运行时根据某些条件将联合类型中的不同成员区分开来,以便能够安全地访问每个成员特有的属性或方法。这通常是通过类型保护来实现的。

interface Cat {  
  kind: 'cat';  
  name: string;  
  purr(): void;  
}  
  
interface Dog {  
  kind: 'dog';  
  name: string;  
  bark(): void;  
}  
  
type Animal = Cat | Dog;  
  
function getAnimalSound(animal: Animal): void {  
  if (animal.kind === 'cat') {  
    // 这里TypeScript知道animal是Cat类型  
    animal.purr();  
  } else if (animal.kind === 'dog') {  
    // 这里TypeScript知道animal是Dog类型  
    animal.bark();  
  }  
}

在这个例子中,kind属性作为“可辨识属性”用于区分Cat和Dog类型。TypeScript能够智能地识别出animal在if语句块中的具体类型,并允许我们调用相应的方法。这就是所谓的“类型收窄”(Type Narrowing)。

总结来说,类型保护和区分类型是在TypeScript中处理联合类型时确保类型安全的重要工具。通过它们,我们可以在运行时安全地访问联合类型中不同成员的属性或方法。