Skip to content

类型系统

结构类型系统

结构类型系统是一种类型系统,它根据对象的结构来确定类型的相等性,而不仅仅是根据类型的名称或标识符。

类型兼容性

  • 如果一个类型包含另一个类型的所有成员,并且它们的类型兼容,则这两个类型被认为是兼容的。
  • 兼容性检查是基于对象的结构而不是名称进行的。
  1. 赋值的兼容
typescript
let x: number;
let y: number | string;

x = 1; // 合法,number 类型的值可以赋给 number 类型的变量
y = 1; // 合法,number 类型的值可以赋给 number | string 类型的变量
  1. 函数参数的兼容
typescript

type Callback = (x: number) => void

let cb1: Callback
let cb2: (y: number | string) => void

cb2 = (input) => {
  console.log(input)
}
cb1 = cb2 // 合法,cb2 的参数类型兼容于 cb1 的参数类型
// (y: number | string) => void 类型的函数,可以赋值给
// (x: number) => void 类型的变量
  1. 接口的兼容

如果一个类型具有另一个类型所需的所有属性,并且属性的类型兼容,那么这两个类型就是兼容的。

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

interface Employee {
    name: string;
    age: number;
    jobTitle: string;
}

let person: Person;
let employee: Employee;

person = employee; 
// 合法,Employee 类型可以赋值给 Person 类型,因为它们的属性结构相似
  1. 可选属性和额外属性的兼容

在接口中,可选属性和额外属性的兼容性规则更加宽松,允许对象具有额外的属性或缺少可选属性。

typescript
interface Person {
    name: string;
    age?: number; // 可选属性
}

let person1: Person;
let person2: { name: string; age: number; city: string };

person1 = person2; // 合法,person2 的额外属性 city 不会引发类型错误

名义类型系统

名义类型系统(Nominal Type System) 有些地方也叫标称类型系统,其实描述的是同一个类型系统。 在名义类型系统中,变量的类型是根据其声明的名称来确定的,而不是根据其结构。也就是说,只有类型的名称完全相同,它们才被认为是相同的类型,即使它们的结构完全相同。

特点:

  1. 基于名称:名义类型系统根据类型的名称来进行类型检查和类型推断,而不是根据类型的结构。
  2. 严格匹配:只有类型的名称完全相同,它们才被认为是相同的类型,即使它们的结构完全相同。

名义类型系统在一些静态类型语言中使用较多,如Java和C#。它强调类型的名称和标识,使得代码更加严格和可靠,但有时也会导致一些冗余的类型声明。与之相比,结构化类型系统更加灵活,允许根据对象的结构进行类型匹配,从而减少了一些类型声明的冗余。

小结

TypeScript 的结构化类型系统是基于类型结构进行比较的,而名义类型系统是基于类型名来进行比较的。

类型拓宽与收窄

类型拓宽(Type Widening)和类型收窄(Type Narrowing)是指在 TypeScript 中根据代码流程的不同而导致变量类型的变化的现象。

类型拓宽

类型拓宽指的是在 TypeScript 中当变量被赋予更宽泛的类型时,其类型会被自动拓宽为更通用的类型的现象

typescript
let x = 10; // 类型推断为 number
x = "hello"; // 类型拓宽为 number | string

类型收窄

类型收窄指的是在 TypeScript 中当我们通过某些方式缩小了变量的类型范围时,其类型会被收窄为更具体的类型的现象

例如,使用类型断言、类型保护、条件语句等方式可以让 TypeScript 在一定程度上确定变量的类型,从而将其类型收窄为更具体的类型

typescript
let x: number | string = 10; // 类型为 number | string
if (typeof x === "number") {
    // 在条件语句中,类型被收窄为 number
    console.log(x.toFixed(2)); // 合法,因为此时 x 是 number 类型
} else {
    // 在另一个分支中,类型被收窄为 string
    console.log(x.toUpperCase()); // 合法,因为此时 x 是 string 类型
}

小结

  • 类型拓宽发生在变量被赋予更宽泛的类型时,其类型会被自动拓宽为更通用的类型。
  • 类型收窄发生在我们通过某些方式缩小了变量的类型范围时,其类型会被收窄为更具体的类型。常见的方式包括类型断言、类型保护和条件语句等

类型系统层级结构

类型层级实际上指的是,TypeScript 中所有类型的兼容关系,从最上面一层的 any 类型,到最底层的 never 类型。那么,从上至下的类型兼容关系到底长什么样呢?

判定类型兼容性的方式

条件类型判断

typescript
type Result = 'wawawa' extends string ? 1 : 0

如果返回1,表示'wawawa'string的子类型,否则不成立。 **T extends SomeType**,表示 T 必须是 SomeType 或其子类型。

赋值判断

通过赋值来进行兼容性检查。

typescript
declare let source: string;

declare let anyType: any;
declare let neverType: never;

anyType = source;

// 不能将类型“string”分配给类型“never”。
neverType = source;

对于变量 a = 变量 b,如果成立,意味着 <变量 b 的类型> extends <变量 a 的类型> 成立,即 b 类型是 a 类型的子类型,在这里即是 string extends never ,这明显是不成立的。

层级结构

字面量类型 < 对应的原始类型

typescript
type Result1 = "yyds" extends string ? 1 : 2; // 1
type Result2 = 1 extends number ? 1 : 2; // 1
type Result3 = true extends boolean ? 1 : 2; // 1
type Result4 = { name: string } extends object ? 1 : 2; // 1
type Result5 = { name: 'yyds' } extends object ? 1 : 2; // 1
type Result6 = [] extends object ? 1 : 2; // 1

// 中间类型略,看下图

顶层类型

顶层类型是所有其他类型的父类型,这意味着在 TypeScript 中的任何类型都可以看作是顶层类型的子类型。TypeScript 中有两个特殊的顶层类型:any 和 unknown

  1. any类型

any类型是 TypeScript 的一个逃生窗口,它可以接受任意类型的值,并且对 any 类型的值进行的任何操作都是允许的。使用 any 类型,可以使我们绕过 TypeScript 的类型检查

typescript
let a: any = 123;  // OK
a = 'hello';  // OK
a = true;  // OK
a = { id: 1, name: 'Tom' }; // OK

a.foo();  // OK
  1. unknown类型

unknown 类型与 any 类型在接受任何类型的值这一点上是一样的,但 unknown 类型却不能像 any 类型那样对其进行任何操作。我们在对 unknown 类型的值进行操作之前,必须进行类型检查或类型断言,确保操作的安全性。

typescript
let u: unknown = 123;  // OK
u = 'hello';  // OK
u = true;  // OK
u = { id: 1, name: 'Tom' }; // OK

// Error: Object is of type 'unknown'.
// u.foo(); 

if (typeof u === 'object' &amp;&amp; u !== null) {
  // OK after type check
  console.log((u as { id: number,

 name: string }).name);
}

底层类型

与顶层类型相对,底层类型是所有类型的子类型。这意味着,在类型系统的层次结构中,任何类型都可以被看作是底层类型的超类型。在 TypeScript 中,never 类型是唯一的底层类型。 never 类型用来表示永远不可能存在的值的类型。比如,一个永远抛出错误或者永远处于死循环的函数的返回类型就是 never

typescript
function error(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}
// 函数 error 和 infiniteLoop 的返回类型都是 never,
// 这是因为这两个函数都永远不会有返回值。

图示

image.png