类型系统
结构类型系统
结构类型系统是一种类型系统,它根据对象的结构来确定类型的相等性,而不仅仅是根据类型的名称或标识符。
类型兼容性
- 如果一个类型包含另一个类型的所有成员,并且它们的类型兼容,则这两个类型被认为是兼容的。
- 兼容性检查是基于对象的结构而不是名称进行的。
- 赋值的兼容
let x: number;
let y: number | string;
x = 1; // 合法,number 类型的值可以赋给 number 类型的变量
y = 1; // 合法,number 类型的值可以赋给 number | string 类型的变量
- 函数参数的兼容
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 类型的变量
- 接口的兼容
如果一个类型具有另一个类型所需的所有属性,并且属性的类型兼容,那么这两个类型就是兼容的。
interface Person {
name: string;
age: number;
}
interface Employee {
name: string;
age: number;
jobTitle: string;
}
let person: Person;
let employee: Employee;
person = employee;
// 合法,Employee 类型可以赋值给 Person 类型,因为它们的属性结构相似
- 可选属性和额外属性的兼容
在接口中,可选属性和额外属性的兼容性规则更加宽松,允许对象具有额外的属性或缺少可选属性。
interface Person {
name: string;
age?: number; // 可选属性
}
let person1: Person;
let person2: { name: string; age: number; city: string };
person1 = person2; // 合法,person2 的额外属性 city 不会引发类型错误
名义类型系统
名义类型系统(Nominal Type System) 有些地方也叫标称类型系统,其实描述的是同一个类型系统。 在名义类型系统中,变量的类型是根据其声明的名称来确定的,而不是根据其结构。也就是说,只有类型的名称完全相同,它们才被认为是相同的类型,即使它们的结构完全相同。
特点:
- 基于名称:名义类型系统根据类型的名称来进行类型检查和类型推断,而不是根据类型的结构。
- 严格匹配:只有类型的名称完全相同,它们才被认为是相同的类型,即使它们的结构完全相同。
名义类型系统在一些静态类型语言中使用较多,如Java和C#。它强调类型的名称和标识,使得代码更加严格和可靠,但有时也会导致一些冗余的类型声明。与之相比,结构化类型系统更加灵活,允许根据对象的结构进行类型匹配,从而减少了一些类型声明的冗余。
小结
TypeScript 的结构化类型系统是基于类型结构进行比较的,而名义类型系统是基于类型名来进行比较的。
类型拓宽与收窄
类型拓宽(Type Widening)和类型收窄(Type Narrowing)是指在 TypeScript 中根据代码流程的不同而导致变量类型的变化的现象。
类型拓宽
类型拓宽指的是在 TypeScript 中当变量被赋予更宽泛的类型时,其类型会被自动拓宽为更通用的类型的现象
let x = 10; // 类型推断为 number
x = "hello"; // 类型拓宽为 number | string
类型收窄
类型收窄指的是在 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 类型。那么,从上至下的类型兼容关系到底长什么样呢?
判定类型兼容性的方式
条件类型判断
type Result = 'wawawa' extends string ? 1 : 0
如果返回1,表示'wawawa'
是string
的子类型,否则不成立。 **T extends SomeType**
,表示 T 必须是 SomeType 或其子类型。
赋值判断
通过赋值来进行兼容性检查。
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
,这明显是不成立的。
层级结构
字面量类型 < 对应的原始类型
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
- any类型
any
类型是 TypeScript 的一个逃生窗口,它可以接受任意类型的值,并且对 any 类型的值进行的任何操作都是允许的。使用 any 类型,可以使我们绕过 TypeScript 的类型检查
let a: any = 123; // OK
a = 'hello'; // OK
a = true; // OK
a = { id: 1, name: 'Tom' }; // OK
a.foo(); // OK
- unknown类型
unknown
类型与 any
类型在接受任何类型的值这一点上是一样的,但 unknown 类型却不能像 any 类型那样对其进行任何操作。我们在对 unknown 类型的值进行操作之前,必须进行类型检查或类型断言,确保操作的安全性。
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' && u !== null) {
// OK after type check
console.log((u as { id: number,
name: string }).name);
}
底层类型
与顶层类型相对,底层类型是所有类型的子类型。这意味着,在类型系统的层次结构中,任何类型都可以被看作是底层类型的超类型。在 TypeScript 中,never
类型是唯一的底层类型。 never
类型用来表示永远不可能存在的值的类型。比如,一个永远抛出错误或者永远处于死循环的函数的返回类型就是 never
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
// 函数 error 和 infiniteLoop 的返回类型都是 never,
// 这是因为这两个函数都永远不会有返回值。