跳至主要內容

高级类型

樱桃茶大约 38 分钟

高级类型

  • instanceof 类型保护
    • instanceof 是 JavaScript 中的一个操作符,用于检查一个对象是否是特定构造函数的实例。
    • 在 TypeScript 中,当你使用instanceof时,它也作为一种类型保护。这意味着在通过instanceof检查后,TypeScript 可以缩小变量的类型到具体的实例类型。
    • 这允许你在代码块中安全地调用实例特有的属性和方法,而不会出现类型错误。
class Bird {
  fly() {
    console.log('Flying');
  }
}

class Fish {
  swim() {
{
    console.log('Swimming');
  }
}

function move(animal: Bird | Fish) {
  if (animal instanceof Bird) {
    animal.fly();
    // TypeScript 现在知道 animal 是 Bird 类型
  } else if (animal instanceof Fish) {
    animal.swim();
    // TypeScript 现在知道 animal 是 Fish 类型
  }
}

const myBird = new Bird();
move(myBird); // 输出: Flying

const myFish = new Fish();
move(myFish); // 输出: Swimming
  • 使用instanceof类型保护时,请确保:
    • 类或构造函数在运行时可用,并且能够被instanceof操作符识别。
    • 类型保护块内部调用正确的成员(属性或方法),以避免运行时错误。

交叉类型(Intersection Types)

交叉类型(Intersection Types)是 TypeScript 中的一种高级类型,允许你将多个类型组合成一个类型。这意味着你可以把多个已存在的类型叠加在一起,创建出拥有所有类型特性的单一类型。

  • 交叉类型用 & 符号定义,例如 Type1 & Type2
  • 结果类型将具备所有参与交叉的类型的特性。如果两种类型都有同样的属性但属性类型不同,则该属性类型被视为 never

下面通过几个例子来理解交叉类型:

interface IEmployee {
  employeeID: number;
  startDate: Date;
}

interface IManager {
  stockPlan: boolean;
}

// 使用交叉类型创建一个新类型,它同时具备IEmployee和IManager的属性。
type ManagementEmployee = IEmployee & IManager;

// 创建一个 ManagementEmployee 类型的对象,它需要包含 IEmployee 和 IManager 所有属性。
let manager: ManagementEmployee = {
  employeeID: 12345,
  startDate: new Date(),
  stockPlan: true,
};

在上面的例子中,ManagementEmployee 类型继承了 IEmployeeIManager 接口的所有属性。任何 ManagementEmployee 类型的变量都必须包含 employeeIDstartDatestockPlan 属性。

另一个例子:

type Point2D = { x: number; y: number };
type Point3D = { z: number };

// 使用交叉类型创建一个三维点类型,合并了二维点和一个额外的z坐标。
type Point = Point2D & Point3D;

// 创建一个 Point 类型的对象, 需要 x, y 和 z 坐标。
let point: Point = {
  x: 1,
  y: 2,
  z: 3,
};

在这个例子中,Point 类型结合了 Point2DPoint3D 的属性,所以一个 Point 类型的对象必须具备 xyz 属性。

最后注意,当你在使用交叉类型时,如果交叉的类型里包含了冲突的属性,如不同类型的同名属性,那么这个属性的类型会被推断为 never 类型,因此实际上无法给这个属性赋值。

type A = { name: string };
type B = { name: number };

// C 的 name 属性类型是 never,因为 string 和 number 是不相容的。
type C = A & B;

// 这里会产生一个错误,因为无法将任何类型的值分配给 never 类型。
let c: C = { name: "TypeScript" }; // Error: Type 'string' is not assignable to type 'never'.

在实际的应用中,避免创建包含冲突属性的交叉类型通常是一个好习惯。

联合类型(Union Types)

联合类型(Union Types)是 TypeScript 提供的一种高级类型,它允许你将多个类型组合在一起,表示一个值可以是几个类型中的任何一个。这在 JavaScript 中是很常见的,因为 JavaScript 是一种动态类型语言,所以一个变量可能在程序执行过程中承载不同类型的值。

  • 基本概念

    • 联合类型使用竖线|分隔每个类型。
    • 可以通过类型检查来保证代码的安全性。
  • 应用实例

    • 假设你有一个函数,它接受字符串或者数字作为参数,那么你可以这样使用联合类型:

      function formatInput(input: string | number) {
        // 函数逻辑
      }
      
    • 当使用联合类型的变量时,只能访问所有类型共有的属性或方法:

      function printId(id: number | string) {
        console.log(id.toString()); // toString() 方法是 number 和 string 类型共有的
      }
      
  • 类型保护

    • 在处理联合类型的值时,通常需要一些方式来检查值的具体类型,这称为类型保护。
    • typeofinstanceof是进行类型保护的常用方法。
  • 例子:使用typeof类型保护:

    function printId(id: number | string) {
      if (typeof id === "string") {
        // 在这个代码块中,id 的类型被视为 string
        console.log(id.toUpperCase());
      } else {
        // 在这个代码块中,id 的类型被视为 number
        console.log(id);
      }
    }
    
  • 例子:使用自定义类型保护函数:

    • 你也可以定义一个函数来检查类型,如果这个函数返回是一个类型断言,则被视为类型保护:

      function isNumber(value: number | string): value is number {
        return typeof value === "number";
      }
      
      function processValue(value: number | string) {
        if (isNumber(value)) {
          // 这里 value 被推断为 number 类型
          console.log(value.toFixed(2));
        } else {
          // 这里 value 被推断为 string 类型
          console.log(value.trim());
        }
      }
      

联合类型是 TypeScript 强大的类型系统特性之一,它可以帮助你写出既灵活又类型安全的代码。在 JavaScript 中,你可能习惯了一个变量可以是任何类型,但在 TypeScript 中,我们更倾向于明确每个变量可能的类型,从而获得编译器的类型检查支持。通过使用联合类型,你可以在保留某些动态特性的同时,确保代码在编译阶段就捕获潜在的错误。

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

类型保护是 TypeScript 的一项特性,用于确保在特定的作用域内变量属于某个特定的类型。通过类型保护,可以让 TypeScript 编译器理解条件检查或类型断言后,变量的具体类型,进而安全地访问该类型的属性或调用其方法。

instanceof 类型保护是使用 instanceof 运算符来判断一个对象是否为某个类的实例。这对于区分不同类型的对象特别有用,尤其是当你需要处理多种可能继承自同一个基类或者实现同一个接口的类型时。

  • 用法示例

    假设我们有两个类 BirdFish,它们都实现了一个共同的接口 Animal,但各自有独特的方法,fly 对于 Birdswim 对于 Fish

    interface Animal {
      live(): void;
    }
    
    class Bird implements Animal {
      live() {
        this.fly();
      }
      fly() {
        console.log("The bird is flying.");
      }
    }
    
    class Fish implements Animal {
      live() {
        this.swim();
      }
      swim() {
        console.log("The fish is swimming.");
      }
    }
    
    function move(animal: Animal) {
      if (animal instanceof Bird) {
        animal.fly(); // 在这里 TypeScript 知道 animal 是 Bird 类型
      } else if (animal instanceof Fish) {
        animal.swim(); // 在这里 TypeScript 知道 animal 是 Fish 类型
      }
    }
    
  • 说明

    上面的 move 函数接收一个 Animal 类型的参数 animal。通过使用 instanceof 类型保护,我们能够在函数内部安全地调用 BirdFish 类型特有的方法 (flyswim)。instanceof 检查之后,TypeScript 编译器能够正确推断出 animal 参数的具体类型,这样我们就不需要任何额外的类型断言,既提高了代码的安全性,也增加了可读性和维护性。

用户自定义的类型保护

用户自定义的类型保护是一种你可以创建的特殊函数或表达式,它们用于确保某个值属于特定的类型。在 TypeScript 中,这通常通过返回一个类型谓词来实现,类型谓词具体形式为 value is Type,其中 value 是参数名字,而 Type 是要检查的类型。

  • 类型谓词

    • 这是 TypeScript 类型系统中的特性。
    • 当你使用类型谓词时,TypeScript 编译器会认为在谓词返回 true 的分支中,相关变量就是指定的类型。
  • 如何编写用户自定义的类型保护

    • 创建一个返回类型为类型谓词的函数。
    • 在实现中,执行运行时检查以确定对象是否符合预期的类型。
    • 返回值必须是一个布尔值。
  • 例子

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

// 用户自定义的类型保护
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

// 使用类型保护
function getSmallPet(): Fish | Bird {
  // ...
}

let pet = getSmallPet();

if (isFish(pet)) {
  pet.swim(); // 现在 TypeScript 知道 pet 是 Fish 类型
} else {
  pet.fly(); // TypeScript 会推断 pet 是 Bird 类型
}

在上面的例子中,isFish 函数是一个用户自定义的类型保护。它检查传入的 pet 是否有 swim 方法来确定它是否为 Fish 类型。当我们在 if 语句中调用 isFish(pet) 时,如果结果为 true,则在该代码块内部,pet 被视为 Fish 类型;否则,在 else 分支中,TypeScript 会认为 petBird 类型。这样通过类型保护,我们可以安全地访问每个接口特有的属性和方法,且不用担心运行时错误。

typeof 类型保护

在 JavaScript 中,typeof是一个操作符,用来获取一个变量或表达式的数据类型。TypeScript 沿用了这个操作器,并将其扩展为一种类型保护机制。这就是typeof类型保护。

  • 类型保护允许你在代码块中确保变量属于某个特定类型。在这些代码块内,你可以安全地假定该变量是这个类型,并且可以访问此类型的属性和方法而不会产生编译错误。
  • TypeScript 认识 JavaScript 中常见的 typeof 运算符返回的值(例如"string", "number", "boolean", "undefined", "function", "object", 和 "symbol"),并且当它与字符串字面量进行比较时,它可以作为类型保障使用。

例如,考虑以下情况,函数接受一个参数,该参数可能是stringnumber类型:

function doSomething(x: string | number) {
  if (typeof x === "string") {
    // 在这个代码块里,x的类型被保护为string
    console.log(x.substr(1)); // String method
  } else {
    // 在这个代码块里,x的类型被保护为number
    console.log(x.toFixed(2)); // Number method
  }
}

在上述示例中:

  • typeof x === "string"是一个类型保护。如果条件为真,则 TypeScript 知道在紧随其后的代码块中,x只能是string类型。
  • 类型保护之后,我们可以安全地调用string类型的方法,如substr,无需担心运行时错误。
  • else分支中,TypeScript 推断出x不是string,因此很可能是number类型(基于我们的参数注释string | number)。
  • 因此,在else代码块中,我们可以调用number对象的toFixed方法。

使用typeof类型保护,可以帮助你编写更安全的代码,减少运行时错误,同时使得代码对于开发者来说更容易理解。

instanceof 类型保护

instanceof 类型保护是 TypeScript 中用来细化类型的一种方式。在 JavaScript 中,instanceof 运算符用于判断一个对象是否是某个构造函数的实例。在 TypeScript 中,它也可以用作类型保护,帮助 TypeScript 编译器理解代码中的类型信息。

  • 当你使用 instanceof 类型保护时,TypeScript 编译器将根据这个检查自动缩小范围到相应的类型。
  • 这意味着在通过 instanceof 检查之后,你可以安全地访问该类型特有的属性和方法,而不会出现编译错误。

示例:

假设有两个类 BirdFish,它们都有各自特有的方法:

class Bird {
  fly() {
    console.log('Flying');
  }
}

class Fish {
  swim() {
{
    console.log('Swimming');
  }
}

现在想要编写一个函数,这个函数接受一个 BirdFish 的实例,并根据实例的类型调用对应的方法:

function move(animal: Bird | Fish) {
  if (animal instanceof Bird) {
    animal.fly(); // TypeScript 知道此处的 animal 是 Bird 类型
  } else {
    animal.swim(); // TypeScript 知道此处的 animal 必须是 Fish 类型
  }
}

在上面的 move 函数中,我们使用了 instanceof 来检查 animal 参数是不是 Bird 类的一个实例。如果是,就调用 fly 方法;如果不是,那么由于类型是 Bird | Fish,TypeScript 推断出 animal 只能是 Fish 类的实例,因此可以安全地调用 swim 方法。

  • 使用 instanceof 类型保护非常适合处理类的层次结构或者当你需要根据实例类型调用特定方法时。

记住,instanceof 类型保护只能用于类或者构造函数,对于接口或其他类型,则需要使用不同的类型保护机制(例如类型谓词)。

可以为 null 的类型

在 TypeScript 中,所有类型默认都是不可以为 null 的。这意味着你不能将 null 赋值给例如 numberstring 或者自定义对象类型的变量,除非你特别声明那个类型可以为 null

  • 基础类型与 null: JavaScript 的基本类型如 numberstring 默认情况下不允许赋值为 null
let myNumber: number = null; // Error: Type 'null' is not assignable to type 'number'
  • 启用严格的空检查: 在 TypeScript 中,通过在 tsconfig.json 中设置 "strictNullChecks": true,可以使编译器对 nullundefined 进行严格的检查。

  • 可选属性和可选参数: 当开启严格空检查时,你可以使用 ? 符号来标记类的属性或者函数的参数为可选的,表明该值可以是 undefined

function greet(name?: string) {
  // ...
}

class Person {
  firstName?: string;
  lastName?: string;
}
  • 联合类型: 为了表示一个值可以是 null,你需要使用联合类型,这是通过类型 | null 来实现的。
let myNumber: number | null = null; // OK, myNumber can be a number or null
myNumber = 5; // Also OK
  • 类型断言: 有时你知道某个值不会是 null 但是 TypeScript 不确信。这时可以使用类型断言来覆盖 TypeScript 的判断。
function processString(s: string | null) {
  console.log(s!.toUpperCase()); // 使用 '!' 告诉TypeScript s 不会是 null
}
  • 类型守卫: 你可能需要使用类型守卫(比如 if 检查)来缩小一个变量的类型范围,确保它不是 null 之后才能调用它的方法。
function processString(s: string | null) {
  if (s !== null) {
    console.log(s.toUpperCase());
  }
}

// 或者使用短路运算符
s && console.log(s.toUpperCase());

理解并正确使用 TypeScript 中的可为 null 类型,可以帮助防止常见的空引用错误,并使得代码更加健売和可维护。

可选参数和可选属性

可选参数和可选属性:

  • 在 JavaScript 中,函数的参数默认是可选的。TypeScript 提供了一种方法来明确地标记一个参数为可选的。

  • 可选参数通过在参数名后加上?来表示。如果函数在调用时没有传递这个参数,它的值就是undefined

    function greeting(name: string, greetingText?: string) {
      if (greetingText) {
        return `${greetingText}, ${name}!`;
      } else {
        return `Hello, ${name}!`;
      }
    }
    
    console.log(greeting("Alice")); // 输出 "Hello, Alice!"
    console.log(greeting("Alice", "Hi")); // 输出 "Hi, Alice!"
    
  • 对于对象类型,你也可以定义可选属性。这意味着该属性可能存在于对象中,也可能不存在。

  • 可选属性同样使用?符号,并且放在属性名后面。

    interface Person {
      name: string;
      age?: number; // age属性是可选的
    }
    
    const bob: Person = { name: "Bob" };
    console.log(bob); // 输出 { name: "Bob" }
    if (bob.age) {
      console.log(`Bob is ${bob.age} years old.`);
    } else {
      console.log("Bob's age is unknown.");
    }
    
  • 使用可选参数和可选属性时要小心,因为它们可能是undefined。在访问前检查它们是否被赋值通常是一个好习惯。

  • TypeScript 的strictNullChecks选项可以使得nullundefined只能赋给any或它们各自的类型 (例如:null只能赋给null类型),这有助于避免错误。

    let foo: string | null = null; // OK with strictNullChecks
    // let bar: string = null; // Error with strictNullChecks
    

    开启strictNullChecks可以强制我们更详细地考虑何时参数或属性可以是nullundefined,并相应地处理它们。

类型保护和类型断言

类型保护和类型断言是 TypeScript 中用来精确地指定一个值的类型的特性,这对于提高代码的可靠性和减少潜在的运行时错误非常有帮助。

  • 类型保护 是一些表达式,它们在运行时检查以确认在某个作用域内的类型。当你对一个变量进行了类型保护后,就可以确定它是特定类型,并且在该作用域内能安全地使用该类型的属性或方法。

    • 使用 typeof 操作符可以保护基本类型。例如,如果你想要保证某个变量是字符串,可以这么写:

      function doSomething(input: string | number) {
        if (typeof input === "string") {
          // 在这个代码块里,input 被 TypeScript 知道是一个字符串
          console.log(input.toLocaleUpperCase());
        } else {
          // 这里 TypeScript 知道 input 是一个数字
          console.log(input.toFixed(2));
        }
      }
      
    • 使用 instanceof 操作符可以保护类实例类型。例如,假设你有两个类 CatDog,都是 Animal 的子类:

      class Cat {
        meow() {
          /* ... */
        }
      }
      class Dog {
        bark() {
          /* ... */
        }
      }
      
      function speak(animal: Cat | Dog) {
        if (animal instanceof Cat) {
          animal.meow(); // TypeScript 知道这是 Cat 实例
        } else {
          animal.bark(); // TypeScript 知道这是 Dog 实例
        }
      }
      
  • 类型断言 允许你告诉编译器:“我知道我正在做什么,我确定这个变量的类型是什么”。类型断言不会进行运行时检查,而是纯粹的编译时语法,因此它不会改变程序的运行时行为。

    • 你可以使用尖括号语法,或者 as 关键字来进行类型断言:

      // 尖括号语法
      let someValue: any = "this is a string";
      let strLength: number = (<string>someValue).length;
      
      // 'as' 语法
      let someOtherValue: any = "this is another string";
      let strOtherLength: number = (someOtherValue as string).length;
      

      注意:在 JSX 中只能使用 as 语法,因为尖括号语法与 JSX 的元素标签相冲突。

  • 类型断言不应随意使用,因为过度使用它可能掩盖真正的类型问题。始终首先考虑使用类型保护来处理不确定的类型。

通过利用类型保护和类型断言,TypeScript 开发者可以编写出既清晰又安全的代码,减少运行时错误的可能性,并使得代码更易于理解和维护。

类型别名

类型别名(Type Aliases)是 TypeScript 中的一种功能,它允许你给一个类型起一个新的名字。这就像是为复杂的结构创建一个更易于理解和使用的简称。类型别名在代码中用type关键字定义。

  • 定义类型别名:

    type Point = {
      x: number;
      y: number;
    };
    

    这个例子中,我们创建了一个名为Point的类型别名,它是一个具有两个数字属性xy的对象。

  • 使用类型别名:

    let center: Point = { x: 0, y: 0 };
    

    在这里,变量center被指定为Point类型,意味着它应该有与Point类型别名匹配的结构。

  • 类型别名与接口的区别: 类型别名与接口(interfaces)类似,但它们有一些细微的差别。类型别名不能被extendsimplements(这是 TypeScript 专用的),而接口可以。另外,某些类型如联合和交叉类型只能用类型别名表示。

  • 联合类型: 类型别名可以用来定义联合类型,即一个值可以是几种类型之一。

    type StringOrNumber = string | number;
    

    在这个例子中,StringOrNumber是一个联合类型,它可以是一个string或一个number

  • 映射类型: 映射类型可以通过类型别名以一种高效的方式根据现有的类型创建新的类型。

    type ReadonlyPoint = Readonly<Point>;
    

    这里我们创建了一个新的类型ReadonlyPoint,它将Point类型的所有属性设置为只读。

  • 泛型别名: 类型别名也可以是泛型的,意味着它们可以接受类型参数。

    type Container<T> = { value: T };
    

    在这个例子中,Container是一个泛型类型别名,它对于任何类型T都会成为一个包含value属性的对象类型。

类型别名是 TypeScript 提供的强大工具,可以帮助你写出更清晰、更可维护的代码。通过为复杂类型或常用模式创建别名,你可以使得代码更容易理解和使用。

接口 vs. 类型别名

在 TypeScript 中,接口(Interfaces)和类型别名(Type Aliases)都可以用来描述对象的形状或者其他复合类型的结构。尽管在很多情况下它们是可以互换使用的,但它们有一些关键性的不同点:

  • 接口(Interfaces):

    • 可以声明合并;如果你声明了两个相同名称的接口,TypeScript 会将它们视为单个接口。
    • 可以被类实现(implements),这是创建一个类以确保遵循特定结构的强大方式。
    • 更适用于定义对象的公共 API。
  • 类型别名(Type Aliases):

    • 是给类型一个新名字的一种方式,但不创建一个新类型。
    • 可以用于其他类型不能用的地方,如原始类型、联合类型、元组。
    • 不能被合并;如果你声明了两个相同名称的类型别名,那将是一个错误。

实用例子

  • 类型别名的例子

    type Point = {
      x: number;
      y: number;
    };
    
    type ID = string | number;
    
  • 接口的例子

    interface Point {
      x: number;
      y: number;
    }
    
    interface Movable {
      move(deltaX: number, deltaY: number): void;
    }
    
    class Car implements Point, Movable {
      x: number;
      y: number;
    
      constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
      }
    
      move(deltaX: number, deltaY: number): void {
        this.x += deltaX;
        this.y += deltaY;
      }
    }
    

在实际应用中,我们通常优先考虑使用接口来定义对象的形状,因为它提供了实现(implement)和扩展(extend)的能力。而当我们需要使用到联合类型或者元组类型时,就可以使用类型别名。

字符串字面量类型

字符串字面量类型是 TypeScript 中的一个高级类型,允许你指定一个变量或者参数只能接受一个特定的字符串值。

  • 字符串字面量类型的作用类似于枚举(Enums),但它专门用于字符串值。通过约束变量为某几个字符串之一,可以确保代码在编译时就避免某些类型错误。

  • 使用字符串字面量类型时,你需要指定变量可能的每一个字符串值。这样,如果试图给该变量赋予一个未列出的字符串值,TypeScript 编译器将会报错。

  • 下面是使用字符串字面量类型的例子:

    type Direction = "up" | "down" | "left" | "right";
    
    function move(direction: Direction) {
      // ...移动逻辑
    }
    
    move("up"); // 正确
    move("down"); // 正确
    move("left"); // 正确
    move("right"); // 正确
    move("north"); // 错误:'north'不是 'up' | 'down' | 'left' | 'right'中的一个
    
  • 字符串字面量类型非常适合模拟字符串枚举或在函数参数中创建约束集。例如,当定义一个接收特定字符串的函数时,可以确保调用者只能传入预期的字符串:

    function createButton(type: "submit" | "reset") {
      // 创建按钮的逻辑,依据 'type' 参数是 'submit' 还是 'reset'
    }
    
    createButton("submit"); // 正确
    createButton("reset"); // 正确
    createButton("button"); // 错误:'button'不是 'submit' | 'reset'中的一个
    
  • 字符串字面量类型也常用于区分具有相同结构但用途不同的对象类型,通常称为“标签联合”(Tagged Unions)或“代数数据类型”(Algebraic Data Types):

    type SuccessResponse = { status: "success"; value: string };
    type ErrorResponse = { status: "error"; error: string };
    
    function handleResponse(response: SuccessResponse | ErrorResponse) {
      if (response.status === "success") {
        console.log("Value received:", response.value);
      } else {
        console.error("Error received:", response.error);
      }
    }
    

在这里,status 字段就是一个字符串字面量类型,它帮助我们区分了两种不同的响应类型,并允许我们在 TypeScript 中安全地处理不同的情况。

数字字面量类型

数字字面量类型是 TypeScript 中的一种高级类型,它允许你将类型定义为具体的数字值。这种类型特别有用,在处理一些具有固定数值集合的场景时可以提供更强的类型检查和自动补全功能。下面通过几个实例讲解数字字面量类型的使用:

  • 定义一个数字字面量类型:

    type Roll = 1 | 2 | 3 | 4 | 5 | 6;
    

    这里定义了一个名为 Roll 的类型,它只能是 1 到 6 之间的整数。这在模拟如骰子这样的场景时非常有用。

  • 应用于函数参数:

    function rollDice(): Roll {
      return (Math.floor(Math.random() * 6) + 1) as Roll;
    }
    

    这个函数 rollDice 模拟了掷骰子的行为,它返回的类型是前面定义的 Roll 类型。由于 Math.random() 返回的是一个随机小数,需要通过转换保证返回值符合 Roll 类型。

  • 使用场景举例 - 控制台日志等级:

    type LogLevel = 1 | 2 | 3 | 4;
    
    function setLogLevel(level: LogLevel) {
      // 设置日志等级的逻辑
    }
    

    在这个例子中,LogLevel 类型限定了日志等级只能是 1, 2, 3, 或 4 这四个数字中的一个。当尝试传递任何不在这个范围内的值给 setLogLevel 函数时,TypeScript 编译器会报错。

  • 结合联合类型扩展使用场景:

    type SuccessCode = 200 | 201 | 202;
    type ErrorCode = 400 | 401 | 404;
    type ResponseCode = SuccessCode | ErrorCode;
    
    function handleResponse(code: ResponseCode) {
      // 根据不同的响应码处理响应
    }
    

    在这个例子中,我们定义了成功的响应码 SuccessCode 和错误的响应码 ErrorCode,然后通过联合类型 (|) 将它们组合成 ResponseCode 类型。这样就可以在函数 handleResponse 中通过类型检查确保传递的响应码是有效的。

数字字面量类型通过使代码变得更加明确和类型安全,帮助开发者避免一些常见的错误,同时也利用 TypeScript 强大的类型系统来提升开发效率。

枚举成员类型

枚举成员类型是 TypeScript 中的一个高级类型概念,它允许你从枚举中获取特定的成员作为类型。在 TypeScript 中,枚举(Enum)是一种特殊的类型,它允许你为一组数值定义友好的名字。

当你定义一个枚举时,每个成员都可以是常量(constant)或计算得出的值。如果枚举成员是带有初始值的常量,那么这些成员本身也可以被当作类型来使用。这意味着你不仅可以使用枚举类型本身作为类型标注,还可以使用枚举中的单个成员作为类型。

以下是一些关于如何使用枚举成员类型的例子:

  • 定义一个简单的数字枚举:

    enum Direction {
      Up = 1,
      Down,
      Left,
      Right,
    }
    
  • 使用枚举成员类型:

    // 这个函数只接受枚举成员类型 Direction.Up 作为参数
    function goUp(direction: Direction.Up) {
      console.log("Going up!");
    }
    
    goUp(Direction.Up); // 正确
    // goUp(Direction.Down); // 错误:Type 'Direction.Down' is not assignable to type 'Direction.Up'.
    
  • 字符串枚举:

    enum FileAccess {
      // 常量成员
      None,
      Read = "READ",
      Write = "WRITE",
      ReadWrite = "READ_WRITE",
    }
    
    // 使用字符串枚举成员类型
    function openFile(access: FileAccess.Read) {
      // 只接受 FileAccess.Read 类型的参数
      console.log("Open file with read access.");
    }
    
    openFile(FileAccess.Read); // 正确
    // openFile(FileAccess.Write); // 错误:Type '"WRITE"' is not assignable to type '"READ"'.
    

通过以上例子,我们可以看到,枚举成员类型使得我们能够限制函数或变量的值必须是枚举中的某个特定成员,提供了更细粒度的控制。这在需要确保函数参数或变量值只能是某些预定义值之一时非常有用。

可辨识联合(Discriminated Unions)

可辨识联合(Discriminated Unions)是 TypeScript 中的一个高级类型用法,它结合了联合类型和字面量类型的特点,使得我们可以在相关联的类型集合中准确地标识出每个类型。这种模式通常用于实现类型安全的模式匹配,特别是在处理不同形状的数据时非常有用。下面通过一些例子来详细解释:

  • 基本概念:在可辨识联合中,每个接口都含有一个共同的属性(称为可辨识的特征或标签),通过这个属性可以确定具体的接口类型。
interface Circle {
  kind: "circle"; // 可辨识的特征
  radius: number;
}

interface Square {
  kind: "square"; // 可辨识的特征
  sideLength: number;
}

// 这里的 Shape 就是一个可辨识联合,包含 Circle 和 Square 两种形态
type Shape = Circle | Square;
  • 使用场景:当你需要根据对象的形状来执行不同的操作但又想保持类型安全时,可辨识联合非常有用。
function getArea(shape: Shape): number {
  switch (
    shape.kind // 通过 kind 属性来辨识具体的形状
  ) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
  }
}
  • 类型保护:在 switch 或者 if 语句中检查 kind 属性,TypeScript 能够理解每个分支中 shape 变量的具体类型。这种技术被称为“类型保护”。
function isCircle(shape: Shape): shape is Circle {
  return shape.kind === "circle";
}

以上就是可辨识联合的核心概念和应用场景。通过这种方式,我们可以编写出既灵活又类型安全的代码,极大地提高了代码的健壮性。

完整性检查

可辨识联合(Discriminated Unions)是 TypeScript 中一种特殊的模式,它结合了单例类型(比如字面量类型)、联合类型、类型守卫,以及类型别名。这个模式允许你处理具有共同的、单一的区分属性的类型集合。这种属性通常被称为标签或者讨论标签。

完整性检查(Exhaustiveness checking)是一个与可辨识联合紧密相关的概念,确保在执行类型相关的代码逻辑时,所有可能的情形都被考虑到了,换言之,没有遗漏任何一个联合成员。

  • 使用 switch 语句进行模式匹配时,我们通过检查标签属性来确定对象具体的类型。

例如:

type Shape = Circle | Square | Rectangle;

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  size: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
  }
}

在这个例子中,kind 属性被用作标签,用于区分不同的形状类型。

  • TypeScript 能够根据覆盖到的情况推断出 switch 语句是否已经做到了完整性检查。如果遗漏了某个情况,TypeScript 编译器将会提示错误。

  • 若要确保完整性检查,可以使用 never 类型,该类型表示不应存在的状态。通过确保所有可能的情况都被处理,我们可以创建一个返回 never 的函数,它对于检测未被处理的 case 是有用的。

例如:

function assertNever(x: never): never {
  throw new Error("Unexpected object: " + x);
}

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.size ** 2;
    case "rectangle":
      return shape.width * shape.height;
    default:
      // If there's an unhandled type, we'll get a compile-time error here
      return assertNever(shape);
  }
}

在这个例子里,如果 Shape 联合类型增加了新的类型而没有更新 getArea 函数,assertNever 函数会因为参数类型不是 never(意味着存在未处理的情况),从而引发编译时错误。这就是所谓的“完整性检查”。

多态的 this 类型

多态的this类型在 TypeScript 中是一种使方法链式调用变得十分方便的特性。这种类型可以很自然地表达出一个继承体系内部,各种方法返回当前类型(而非某个固定的父类型)的模式。

  • 基本概念:当一个类或接口拥有返回值为当前类型(this)的方法时,你就可以利用多态的this类型。这意味着继承自该类(或实现了该接口)的任何子类都能够使用这些方法,并且这些方法返回的将是具体的子类类型,而非父类。

  • 应用场景:这项功能主要在需要流畅的链式调用时显得非常有用,例如,在构建一个可链式调用的 API 或者框架时,比如许多 UI 库或者数据处理库。

例子说明

假设我们有一个简单的类结构,意在通过链式调用设置对象的属性。

class Calculator {
  protected value: number = 0;

  // 使用多态的this类型
  public add(n: number): this {
    this.value += n;
    return this;
  }

  public multiply(n: number): this {
    this.value *= n;
    return this;
  }

  public getValue(): number {
    return this.value;
  }
}

// 扩展Calculator,添加一个新的方法
class AdvancedCalculator extends Calculator {
  public square(): this {
    this.value = this.value * this.value;
    return this;
  }
}

const calc = new AdvancedCalculator();
console.log(calc.add(5).multiply(2).square().getValue());
// 这里add, multiply和square方法均返回实例本身(this),允许方法连续调用,并且square方法在AdvancedCalculator类中定义,返回的也是AdvancedCalculator的实例。

在上述例子中:

  • Calculator类有两个方法addmultiply,它们都返回this类型,即当前类的实例。
  • AdvancedCalculator继承自Calculator,并添加了一个新方法square也返回this类型。
  • 因为这些方法返回this,它们支持链式调用。当我们从AdvancedCalculator实例调用这些方法时,它们不仅可以无缝地工作,而且square方法返回的还是AdvancedCalculator类型的实例,保留了该类型的所有方法,这就是多态的this类型的魅力。
  • 这种方式大大增加了代码的可读性和易用性,尤其在建立复杂对象时,能够提供更加流畅和直观的 API 设计。

多态的this类型让对象方法的返回类型能够更准确地反映出实际调用链的情况,对于设计链式 API 来说是一个非常有价值的特性。

索引类型(Index types)

TypeScript 的索引类型是一种高级类型,它允许我们使用其他类型的值来索引对象属性。索引类型可以帮助确保我们不会错误地引用一个不存在的属性,同时还能够保证当我们索引对象时,得到的类型是正确的。

在详细解释索引类型之前,了解以下几个概念对理解很重要:

  • 索引类型查询操作符 keyof: 用来获取某种类型的所有键,其结果为这些键名称的联合类型。
  • 索引访问操作符 []: 用来访问某个类型的子类型。

索引类型(Index types)

  • 使用 keyof T 获取类型 T 的所有公共属性名组成的联合类型。

    interface Person {
      name: string;
      age: number;
    }
    
    type PersonKeys = keyof Person; // "name" | "age"
    
  • 如果你有一个类型,并且想要通过这个类型的属性去索引另一个接口,那么可以使用 T[K] 这样的语法。

    function getProperty<T, K extends keyof T>(obj: T, key: K) {
      return obj[key]; // 返回类型为 T[K]
    }
    
    let person: Person = {
      name: "Alice",
      age: 25,
    };
    
    let name: string = getProperty(person, "name"); // 正确
    let age: number = getProperty(person, "age"); // 正确
    // let unknown = getProperty(person, 'unknown'); // 错误,'unknown' 不在 'name' | 'age' 类型上
    

    在这个例子中,getProperty 函数接受一个对象和一个键,然后返回对象上该键对应的值。类型 K 被约束为 keyof T,意味着 K 只能是类型 T 的属性键集合中的一个。

  • 索引类型和字符串索引签名如 Record<K, T> 结合得非常好,表示一个对象的所有属性都是同一类型。

    interface PhoneBook {
      [name: string]: string;
    }
    
    let phones: Record<string, string> = {};
    phones["Alice"] = "123-456-7890";
    // phones[10] = '234-567-8901'; // 错误:索引签名只接受 string
    

    在这个 PhoneBook 接口中,我们定义了一个字符串索引签名,表示每一个属性的值都应该是一个字符串。换句话说,一个 PhoneBook 是一个从字符串到字符串的映射。

  • 使用索引类型和映射类型将一个类型的属性转换成其他类型。

    type Readonly<T> = {
      readonly [P in keyof T]: T[P];
    };
    
    type Partial<T> = {
      [P in keyof T]?: T[P];
    };
    
    type PersonPartial = Partial<Person>; // { name?: string; age?: number; }
    type ReadonlyPerson = Readonly<Person>; // { readonly name: string; readonly age: number; }
    

    这里 ReadonlyPartial 是 TypeScript 中内置的工具类型。Readonly 把所有属性都变为只读,Partial 则使所有属性变为可选。这两种类型都用到了索引类型,在映射类型中通过 in keyof T 遍历类型 T 的所有属性,然后应用相应的类型变换。

索引类型提供了强大的方式来动态地处理对象的属性和保证类型安全性。通过结合泛型、keyof 和索引访问类型,开发者可以编写灵活且类型安全的代码。

索引类型和字符串索引签名

索引类型(Index Types)允许我们使用动态的属性名来获取对象上的值。在 TypeScript 中,我们可以使用索引类型查询和索引访问操作符。

  • 索引类型查询(keyof
    • keyof T的结果是T上已知的公共属性名的联合。
    • 如果你有一个类型Tkeyof T将会产生一个字符串或数字(取决于索引签名)的联合类型,这个联合类型包含了T所有的公共键。
interface Person {
  name: string;
  age: number;
}

let personProps: keyof Person; // 'name' | 'age'
  • 索引访问操作符(T[K]
    • 使用类型T和属性名K时,T[K]将会返回属性K对应的类型。
    • 这种方式类似于 JavaScript 中的动态访问属性obj[propName]
function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

const person = {
  name: "Alice",
  age: 25,
};

const name: string = getProperty(person, "name");
const age: number = getProperty(person, "age");
  • 字符串索引签名
    • 当你不确定对象将会有哪些属性时,可以使用字符串索引签名。
    • 它通常被写成:[index: string]: Type
    • 这表示对象可以拥有任意数量的属性,只要它们都是同一类型。
interface AnyProps {
  [key: string]: any;
}

const obj: AnyProps = {
  anything: 100,
  everything: "yes",
};

obj.anything; // Works, type is 'any'
obj.everything; // Works, type is 'any'
  • 索引类型和字符串索引签名可以结合使用
    • 可以通过索引类型查询得到一个类型,并且也可以定义一个具有字符串索引签名的类型。
    • 在处理复杂数据结构时特别有用,比如当对象的键是动态的。
interface Dictionary<T> {
  [key: string]: T;
}

const dictionary: Dictionary<number> = {
  ten: 10,
  twenty: 20,
  thirty: 30,
};

const keys: keyof Dictionary<number>; // string | number
const value: Dictionary<number>["ten"]; // number

以上就是 TypeScript 中关于索引类型和字符串索引签名的解释,通过这些高级特性,你可以更灵活地编写和使用带有动态键的对象类型。

映射类型

映射类型是 TypeScript 提供的一个非常有用的高级类型功能,允许你从旧类型中创建新类型,其方式是对旧类型的属性进行某种操作。这对于在很多不同情况下保持你的代码 DRY(Don't Repeat Yourself)非常有帮助,特别是处理对象结构时。

  • 基本形式

    • 映射类型在最简单的形式下看起来像是对一个已知类型的每个属性应用相同的修改。
    type ReadOnly<T> = {
      readonly [P in keyof T]: T[P];
    };
    

    这里,ReadOnly类型会将某个类型T的所有属性变为只读。

  • 利用映射类型修改属性

    • 除了使属性只读,你还可以利用映射类型让所有属性变为可选的或其他任何形态。
    type Optional<T> = {
      [P in keyof T]?: T[P];
    };
    

    这会将类型T的所有属性变成可选的。

  • 使用as来重映射键

    • TypeScript 4.1 及以上版本支持使用as在映射类型中重新映射键名。
    type MappedTypeWithNewProperties<T> = {
      [K in keyof T as NewKeyType]: T[K];
    };
    

    这样可以在创建映射类型的同时改变属性的名称。

  • 利用预定义的条件类型

    • TypeScript 内置了一些工具类型来帮助创建常见的映射类型,例如Partial<T>Readonly<T>Record<K,T>Pick<T,K>等。
    • Partial<T>将类型T的所有属性转换为可选属性。
    • Readonly<T>将类型T的所有属性转换为只读属性。
    • Record<K,T>创建一个类型,其属性键为K,属性值为T
    • Pick<T,K>从类型T中选择一组属性K来构造类型。
  • 高级映射技巧

    • 映射类型还可以与联合类型、交叉类型和其他高级类型结合使用,来创建更复杂的类型变换。
    type Nullable<T> = { [P in keyof T]: T[P] | null };
    type PartialAndNullable<T> = Partial<Nullable<T>>;
    

    这里,Nullable首先将类型T的所有属性变为当前类型或null,然后PartialAndNullable将这些属性都变成可选的。

映射类型是理解和掌握 TypeScript 强大功能的关键之一,他们提供了一种灵活的方法来根据现有的类型生成新的类型,极大地增加了 TypeScript 的表达能力。通过上面的例子,你可以开始尝试创建自己的映射类型以适应不同的编程需求。

由映射类型进行推断

映射类型允许你基于旧的类型创建新的类型,可以将一个已知的类型的每个属性进行转换。在 TypeScript 中,你可以使用它来表示一个由同一类型的值组成的对象,但是具体的键可以是不同的。

当你有映射类型时,你可能想要根据这个映射类型去推断出原始类型。TypeScript 提供了一种叫做“由映射类型进行推断”(Infer from Mapped types)的能力,以便从这样的类型中恢复出单个属性的类型。

下面通过例子来解释:

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

// 创建一个映射类型,其中属性值变为了包含该属性值的数组
type PersonArrays = { [K in keyof Person]: Person[K][] };

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key]; // 返回类型是 T[K]
}

// 假设我们想要根据PersonArrays推断出Person的某个属性的类型
function unboxArrayProperty<T, K extends keyof T>(
  obj: T,
  key: K
): T[K] extends (infer U)[] ? U : T[K] {
  const prop = getProperty(obj, key);
  if (Array.isArray(prop)) {
    return prop[0]; // 如果是数组返回第一个元素
  }
  return prop; // 否则直接返回
}

const people: PersonArrays = {
  name: ["Alice", "Bob"],
  age: [30, 25],
};

const name = unboxArrayProperty(people, "name"); // 类型是 string
const age = unboxArrayProperty(people, "age"); // 类型是 number

在上面的unboxArrayProperty函数中,使用了条件类型和infer关键字来推断映射类型内部数组元素的类型。如果T[K]是一个数组,那么就会提取出数组内部的元素类型U,然后函数返回U类型的值;如果T[K]不是数组,则直接返回该类型的值。

这种技术在处理包装过的类型或者想要根据映射类型逆向获得原始类型信息时非常有用。通过这种方式,你可以编写更加通用和可重用的函数。

预定义的有条件类型

映射类型允许你根据一个既定的类型创建新的类型,它以某种模式去“映射”每个属性。在了解预定义的有条件类型之前,我们应该先知道 TypeScript 提供了一些有条件的类型工具,它们像是可用于类型系统中的逻辑运算符。

预定义的有条件类型

在 TypeScript 中,预定义的有条件类型帮助我们在类型系统中做判断,从而使类型更加动态和灵活。这里列举了一些常见的预定义有条件类型及其用法:

  • Exclude<T, U>:从T可分配的类型中排除U

    • 例子:Exclude<"a" | "b" | "c", "a">结果为"b" | "c"
  • Extract<T, U>:选取T中可以分配给U的类型。

    • 例子:Extract<"a" | "b" | "c", "a" | "f">结果为"a"
  • NonNullable<T>:从T中排除nullundefined

    • 例子:NonNullable<string | number | undefined>结果为string | number
  • ReturnType<T>:获取函数T的返回类型。

    • 例子:ReturnType<() => string>结果为string
  • InstanceType<T>:获取构造函数类型T的实例类型。

    • 例子:class MyClass { x: number; }; InstanceType<typeof MyClass>结果为MyClass的实例类型。
  • Required<T>:将T中所有属性设置为必需的,即不包含undefined

    • 例子:interface MyInterface { a?: number; b: string; }; Required<MyInterface>结果为 { a: number; b: string; }
  • Partial<T>:将T中所有属性设置为可选的。

    • 例子:interface MyInterface { a: number; b: string; }; Partial<MyInterface>结果为 { a?: number; b?: string; }
  • Readonly<T>:将T的所有属性设置为只读,不能被重新赋值。

    • 例子:interface MyInterface { a: number; b: string; }; Readonly<MyInterface>结果为 { readonly a: number; readonly b: string; }
  • Record<K, T>:创建一个类型,其属性键是K,属性值是T

    • 例子:Record<"a" | "b", number>结果为 { a: number; b: number; }

使用这些预定义的类型,你可以构建更复杂、根据条件变化的类型,这对于处理各种不同情况的输入或输出类型非常有用。

示例

映射类型允许你根据一个已存在的类型创建新的类型,其属性是基于这个已存在的类型的属性变换而来。当使用映射类型进行类型推断时,TypeScript 提供了一些预定义的有条件类型(Conditional Types),它们在处理泛型和复杂类型转换时非常有用。

预定义的有条件类型包括:

  • Exclude<T, U>:从 T 中排除那些可以赋值给 U 的类型。
  • Extract<T, U>:选择 T 中那些可以赋值给 U 的类型。
  • NonNullable<T>:从 T 中排除 null 和 undefined。
  • ReturnType<T>:获取函数类型 T 的返回类型。
  • InstanceType<T>:获取构造函数类型 T 的实例类型。

示例:

type T0 = Exclude<"a" | "b" | "c", "a">; // "b" | "c"
type T1 = Extract<"a" | "b" | "c", "a" | "f">; // "a"
type T2 = NonNullable<string | number | null | undefined>; // string | number
type T3 = ReturnType<() => string>; // string
type T4 = InstanceType<typeof Date>; // Date

在这些示例中:

  • T0"b" | "c" 因为 "a" 被排除了。
  • T1"a" 因为只有 "a" 同时存在于两个联合类型中。
  • T2string | number 因为 nullundefined 被排除了。
  • T3string 因为这是函数返回值的类型。
  • T4Date 类的实例类型。

通过运用这些预定义的有条件类型,可以更灵活地控制类型的行为,帮助你在 TypeScript 中编写出更精确且强大的类型系统。