# TS 的类型检查机制,类型兼容性、保护机制、高级类型
TIP
从本节内容开始,正式学习 Typescript 中的类型检查机制,高级类型,相关配置及应用等。
- TS 的类型检查机制
- 类型的兼容性
- TS 的类型保护机制
- TS 的高级类型
- TS 核心基础知识总结
# 一、TS 的类型检查机制
TIP
TypeScript 编译器在做类型检查时,所秉承的一些原则,以及表现出的一些行为。即:TS 的类型检查机制
作用: 辅助开发,提高开发效率
从以下几个方面来学习
- 类型推断
- 类型兼容性
- 类型保护
# 1、类型推断
TIP
不需要指定变量的类型(函数的返回值类型),TypeScript 可以根据某些规则自动地为其推断出一个类型。
- 基础类型推断
- 最佳通用类型推断
- 上下文类型推断
# 1.1、基础类型推断
TIP
基础类型推断也是 TS 中比较常见的类型推断,通常发生在以下场景中
- 初始化变量时
- 设置函数默认参数时
- 在确定函数返回值时
初始化变量时
// 初始化变量时,声明一个变量 a
// 如果不指定它的变量类型,TS 会自动推断为 any 类型
let a;
// 如果给变量 a 赋值为 1,就会推断为 number 类型
let a = 1;
// 给变量 b 赋值为 空数组,b 就会被推断为以 any 类型为元素的 数组类型
let b = [];
// 给 b 赋值为 [1, 2],就会被推断为 number 类型的数组
let b = [1, 2];
设置函数默认参数时
// 定义一个函数 c ,为它指定一个默认的参数 x,值为 1,x 被推断为 number 类型
let c = (x = 1) => {};
在确定函数返回值时
// 将该函数返回 x + 1 ,c 的返回值类型就会被 推断为 number 类型
let c = (x = 1) => x + 1;
# 1.2、最佳通用类型推断
TIP
当需要从多个类型中推断出一个类型时,TS 就会尽可能的推断出一个兼容当前所有类型的通用类型,这个就是最佳通用类型推断
// 给数组增加一个 null,null 和 number 类型是不兼容的
// 因此,d 就会被推断为 null 和 number 的联合类型
let d = [1, null];
如果在 tsconfig.json
配置文件中关闭 "strictNullChecks": false
{
"compilerOptions": {
"strictNullChecks": false
}
}
此时,以上 null 和 number 就兼容了,d 就会被推断为 number 类型的数组
// 修改完配置文件后,d 就会被推断为 number 类型的数组
let d = [1, null];
以上的类型推断都是从右向左的推断,也就是根据表达式右侧的值来推断表达式左侧的变量的类型
# 1.3、上下文类型推断
TIP
上下文类型的推断是从左到右的,它通常会发生在一个事件处理中
// 给 window 绑定一个 onkeydown 事件,这时就会发生上下文的类型推断
// TS 会根据左侧的事件绑定,来推断出右侧的事件类型,会被推断为 KeyboardEvent 类型(鼠标划上 event 即可看到)
window.onkeydown = (event) => {
// 打印输出 event 键盘事件有哪些属性
// event.button 会报错:类型“KeyboardEvent”上不存在属性“button”,因为 button 不是键盘事件的属性,而是鼠标事件的属性
console.log(event.button);
};
# 2、类型断言
TIP
有时候,TS 的类型推断可能不符合你的预期,而且你完全有自信比 TS 更了解你的代码。
这时 TS 就提供了一种方法,允许你覆盖它的推断,即:类型断言
// 定义一个空对象
let foo = {};
// 给对象指定 bar 属性,此时会报错:类型“{}”上不存在属性“bar”
foo.bar = 1;
如何解决以上的报错问题呢 ?
// 定义一个接口
interface Foo {
// 给接口定义一个 bar 属性
bar: number;
}
// 利用 类型断言将 foo 指定为一个 Foo 接口类型,这时就不会报错了(类型断言就发挥作用了)
let foo = {} as Foo;
foo.bar = 1;
同时,也要注意类型断言不能乱用
interface Foo {
bar: number;
}
// let foo = {} as Foo
// 比如:去掉该赋值语句
// 这时没有报错,但 foo 对象并么有按照接口的严格规定给其赋值为一个 bar 属性
// foo.bar = 1
// 因此在声明时,就指定类型
let foo: Foo = {
// TS 要求必须添加接口中的属性
bar: 1,
};
注:
类型断言,可以增加代码的灵活性,再改造一些旧代码时会非常有效。
但使用类型断言要注意避免滥用,需要对上下文环境有充分的预判,没有任何根据的类型断言,会给我们的代码带来安全隐患。
# 3、总结
TIP
TS 的类型推断我们可以为我们提供重要的辅助信息,应该深加利用。
我们也可以检查下之前写的代码,观察哪里部分可以使用类型推断进行优化。
# 二、类型的兼容性
TIP
当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y
其中,X 被叫做 目标类型,Y 被叫做 源类型
X 兼容 Y :X(目标类型)= Y(源类型)
// X 兼容 Y :X(目标类型)= Y(源类型)
// 定义一个字符串变量 s
let s: string = "ibc";
// 当关闭 tsconfig.json 配置文件中为 "strictNullChecks": false 时
// 字符串变量是可以被赋值为 null 的,此时我们可以说字符型是可以兼容 null 类型的
// 即:null 是字符型的子类型
s = null;
注:
之所以,我们要讨论类型兼容性问题,是因为 TS 允许我们把一些类型不同的变量相互赋值。
虽然在某种程度上讲会产生不可靠的行为,但却增加了语言的灵活性,而类型兼容性的例子会广泛存在于 接口、函数 和 类中。
# 1、接口兼容性
TIP
下面我们来观察两个接口是如何兼容的
// 定义接口 X ,其中有两个属性
interface X {
a: any;
b: any;
}
// 定义接口 Y ,其中有三个属性
interface Y {
a: any;
b: any;
c: any;
}
// 定义变量 x 和 y 分别符合以上接口类型
let x: X = { a: 1, b: 2 };
let y: Y = { a: 1, b: 2, c: 3 };
// 思考 x 和 y 可以相互赋值吗
// x 是可以被赋值给 y
x = y;
// y 不能被赋值为 x(会报错)
y = x;
注:
只要 Y 接口具备 X 接口的所有属性(即:a 和 b),即使有 额外的属性 c ,y 仍然可以被认为是 X 类型。即 X 类型可以兼容 Y 类型
这里再次体现了 TS 的类型检查原则,即:鸭式辨型法
“一只鸟,看起来像鸭子,游起来像鸭子,叫起来像鸭子,那么这只鸟就可以被认为是鸭子”。总结:语言类型必须具备目标类型的必要属性,就可以进行赋值。
总结一句口诀:接口之间相互兼容时,成员少的兼容成员多的
# 2、函数兼容性
TIP
函数之间兼容的条件有以下三个
- 参数个数
- 参数类型
- 返回值类型
需要判断两个函数是不是兼容,通常会发生在两个函数相互赋值的情况下
当函数作为参数时
// 函数兼容性
// 定义函数类型 Handler,有两个参数 a 和 b
type Handler = (a: number, b: number) => void;
// 定义一个高阶函数,该函数以 Handler 类型为参数,并直接返回
function hof(handler: Handler) {
return handler;
}
注:
当我们给以上高阶函数传入一个参数时,就会判断该参数是否 和 Handler 类型兼容,Handler 为目标类型,传入的参数为 源类型。
如果要目标函数 兼容 源函数,它们需要同时满足三个条件(如下)
# 2.1、参数个数
// 函数兼容性
// 定义函数类型 Handler,有两个参数 a 和 b
type Handler = (a: number, b: number) => void;
// 定义一个高阶函数,该函数以 Handler 类型为参数,并直接返回
function hof(handler: Handler) {
return handler;
}
/**
* 当我们给以上高阶函数传入一个参数时,就会判断该参数是否 和 Handler 类型兼容
* Handler 为目标类型,传入的参数为 源类型
* 如果要目标函数 兼容 源函数,它们需要同时满足三个条件(如下)
**/
// 1、参数个数(要求目标函数的参数个数一定要多余源函数的参数个数)
// 定义一个只有一个参数的函数
let handler1 = (a: number) => {};
// 将 handler1 作为参数传递给 hof 高阶函数,没问题(因为它只有一个参数)
hof(handler1);
// 定义一个有 3个 参数的函数
let handler2 = (a: number, b: number, c: number) => {};
// 将 handler2 作为参数传递给 hof 高阶函数(会报错,因为它的参数是三个,而目标函数只有两个参数)
hof(handler2);
注:
以上情况都是具有固定参数的函数,如果函数中含有可变参数(可选参数)或剩余参数时,会遵循其它的原则
可选参数 和 剩余参数
// 定义函数 a ,为固定参数
let a = (p1: number, p2: number) => {};
// 定义函数 b,为可选参数
let b = (p1?: number, p2?: number) => {};
// 定义函数 c,为剩余参数
let c = (...args: number[]) => {};
// 原则一:固定参数是可以兼容可选参数 和 剩余参数的
a = b;
a = c;
// 原则二:可选参数是不兼容固定参数 和 剩余参数的(通过关闭 tsconfig.json 中 "strictFunctionTypes": false 选项即可实现兼容)
b = c;
b = a;
// 原则三:剩余参数可兼容 固定参数 和 可选参数
c = a;
c = b;
注:
以上是函数之间兼容必须要满足的第一个条件,即参数个数的要求。
函数兼容性的第二个条件,如下
# 2.2、参数类型
TIP
函数兼容性的第二个条件:参数类型,要求必须匹配
// 定义函数类型 Handler,有两个参数 a 和 b
type Handler = (a: number, b: number) => void;
// 定义一个高阶函数,该函数以 Handler 类型为参数,并直接返回
function hof(handler: Handler) {
return handler;
}
// 参数类型
// 定义一个只有一个参数的函数
let handler3 = (a: string) => {};
// 将 handler3 作为参数传递给 hof 高阶函数(会报错:因为它的类型是不兼容的)
hof(handler3);
对象类型
// 定义接口 Point3D 有3个属性
interface Point3D {
x: number;
y: number;
z: number;
}
// 定义接口 Point2D 有2个属性
interface Point2D {
x: number;
y: number;
}
// 定义两个函数 p3d 和 p2d,它们的参数是上边定义的接口类型
// 以下函数的 参数个数是相同的,参数的类型都是 对象
let p3d = (point: Point3D) => {};
let p2d = (point: Point2D) => {};
// 它们之间的兼容性如何 ?
// p3d 兼容 p2d
p3d = p2d;
// p2d 不兼容 p3d(观察两个函数的参数,它们都是对象:p3d 中有3个成员,p2d 中有2个成员)
// 即:成员个数多的 兼容 成员个数少的(与之前接口之间兼容性的结论正好相反)两个接口之间兼容要求成员少的 兼容 成员多的
// 这里正好相反,很容易混淆(可以用另一种方法来看:不要把 Point3D 接口对象看成一个整体的对象,可以拆分成 3个参数,即参数多的兼容参数少的,与之前的结论就一致了)
p2d = p3d;
如果一定要 p2d 兼容 p3d 也是可以做到的,将 tsconfig.json
中配置设为 false,这样两个函数就可以兼容了
{
"compilerOptions": {
"strictFunctionTypes": false
}
}
注:
以上这种函数的参数之间可以相互赋值的情况,叫做 函数参数双向协变
它允许我们把一个精确的类型,赋值给一个不那么精确的类型,这样做很方便。我们就不需要把一个不精确的类型断言成一个精确的类型了。
# 2.3、返回值类型
TIP
TS 要求目标返回值类型 必须 与原函数的返回值类型相同 或 为其子类型
// 定义函数 f,它的返回值是一个对象,其中有一个字段 username
let f = () => ({ username: "ibc" });
// 定义函数 g,它的返回值有两个字段
let g = () => ({ username: "ibc", location: "Beijing" });
// f 就可以兼容 g
f = g;
// g 是不兼容 f 的(因为 f 的返回值类型是 g 返回值类型的 子类型)
g = f;
// 同样这里也是成员少的会兼容成员多的,与 鸭式辨型法 是一致的
函数重载
TIP
函数重载分为两个部分
- 第一部分:函数重载的列表。如:在列表中定义了两个函数,overload
- 第二部分:函数的实现。
这里,列表中的函数就是 目标函数,而具体的实现就是 原函数。程序在运行时编译器会查找重载的列表,然后使用第一个匹配的定义来执行下面的函数。
所以,在重载列表中目标函数的参数要多于原函数的参数,而且返回值类型也要符合相应的要求。
function overload(a: number, b: number): number;
function overload(a: string, b: string): string;
function overload(a: any, b: any): any {}
// 在具体实现时,增加一个参数 c ,这样就是不兼容的(因为实现的参数的个数 多于 目标函数的个数了)
function overload(a: any, b: any, c: any): any {}
// 去掉返回值,它也是不兼容的(返回值类型不兼容)
// function overload(a: any, b: any) {}
注:
以上是两个函数之间要兼容,所必须要满足的三个条件
# 3、枚举类型兼容性
TIP
枚举类型 和 数值类型 是可以完全互相兼容的
// 枚举兼容性
enum Fruit {
Apple,
Banana,
}
enum Color {
Red,
Yellow,
}
// 枚举类型 和 数值类型 是可以完全互相兼容的
// 定义变量 fruit 它的类型是 一个枚举类型,可以给它赋值任意的数字
let fruit: Fruit.Apple = 2;
// 定义变量 no 它的类型是 一个数值类型,它可以被赋值给一个枚举类型
let no: number = Fruit.Apple;
// 因此,我们可以看到枚举 和 数值类型是可以相互兼容的
枚举 和 number 之间是可以兼容的,枚举之间是不兼容的
// 枚举兼容性
enum Fruit {
Apple,
Banana,
}
enum Color {
Red,
Yellow,
}
// 枚举之间是完全不兼容的(如下:Color 和 Fruit 是完全不兼容的)
let color: Color.Red = Fruit.Apple;
# 4、类的兼容性
TIP
类的兼容性 与 接口比较相似,它们也是只比较结构
注意: 在比较两个类是否兼容时,静态成员 和 构造函数是不参与比较的。如果两个类具备两个相同的实例成员,它们的实例就可以完全相互兼容
// 定义两个类 A 和 B
class A {
constructor(a: number, b: number) {}
id: number = 1;
}
class B {
static s = 1;
constructor(a: number) {}
id: number = 2;
}
// 分别创建了两个实例 aa 和 bb
let aa = new A(1, 3);
let bb = new B(1);
// 这两个实例是完全兼容的,因为它们都具有两个实例 id
// 而构造函数 和 静态成员是不作为比较的
aa = bb;
bb = aa;
如果类 A 和 B 含有私有成员呢 ?
// 定义两个类 A 和 B
class A {
constructor(a: number, b: number) {}
id: number = 1;
// 添加一个私有成员
private username: string = "";
}
class B {
static s = 1;
constructor(a: number) {}
id: number = 2;
// 添加一个私有成员
private username: string = "";
}
// 分别创建了两个实例 aa 和 bb
let aa = new A(1, 3);
let bb = new B(1);
// 以上 A 和 B 类中含有私有成员,这两个类就不兼容了(会报错)
aa = bb;
bb = aa;
注:
如果类中含有私有成员,这两个类就不兼容了
此时,只有父类和子类之间是可以相互兼容的
定义一个 A 的子类
// 定义两个类 A 和 B
class A {
constructor(a: number, b: number) {}
id: number = 1;
// 添加一个私有成员
private username: string = "";
}
class B {
static s = 1;
constructor(a: number) {}
id: number = 2;
// 添加一个私有成员
private username: string = "";
}
// 分别创建了两个实例 aa 和 bb
let aa = new A(1, 3);
let bb = new B(1);
// aa = bb
// bb = aa
// 如果类中含有私有成员,这两个类就不兼容了
// 此时,只有父类和子类之间是可以相互兼容的
// 定义一个 A 的子类 C
class C extends A {}
// 创建 C 的实例
let cc = new C(1, 2);
// 父类 和 子类的实例是可以完全相互兼容的
aa = cc;
cc = aa;
# 5、泛型的兼容性
// 泛型兼容性
// 定义一个泛型接口 Empty ,该接口没有任何的成员
interface Empty<T> {}
// 定义 obj1 和 obj2 两个变量,该变量都是 Empty 接口类型(其中传入的参数类型不同)
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
// 这两个变量是相互兼容的,因为 Empty 接口中没有任何的成员
obj1 = obj2;
给 Empty 泛型接口中,添加成员
// 给泛型接口 Empty 中添加一个成员
interface Empty<T> {
value: T;
}
let obj1: Empty<number> = {};
let obj2: Empty<string> = {};
// 这是两个变量就不兼容了(会报错)
// 也就是说只有类型参数 T 被接口成员使用时,才会影响泛型的兼容性
obj1 = obj2;
# 6、泛型函数
// 以下定了两个完全相同的泛型函数
let log1 = <T>(x: T): T => {
console.log("x");
return x;
};
let log2 = <U>(y: U): U => {
console.log("y");
return y;
};
// log1 和 log2 是完全兼容的
// 即:如果两个泛型函数的定义相同,但没有指定类型参数,那么它们之间也是可以相互兼容的
log1 = log2;
注:
TS 允许我们在类型兼容的变种之间相互赋值,这个特性增加了语言的灵活性。
# 7、总结
TIP
关于类型兼容性的部分学习了很多规则,我们总结了几句比较好记的几条兼容性口诀:
当一个类型 Y 可以被赋值给另一个类型 X 时,我们就可以说类型 X 兼容类型 Y
其中,X 被叫做 目标类型,Y 被叫做 源类型
口诀:
- 结构之间兼容:成员少的兼容成员多的
- 函数之间兼容:参数多的兼容参数少的
# 三、TS 的类型保护机制
我们先来观察如下代码
enum Type {
Strong,
week,
}
// 定义 Java 类
class Java {
helloJava() {
console.log("Hello Java");
}
}
// 定义 JavaScript 类
class JavaScript {
helloJavaScript() {
console.log("Hello JavaScript");
}
}
// 定义一个函数
function getLanguage(type: Type) {
// 判断该类型是 强类型 则返回 Java 类的实例,否则 返回 JavaScript 类的实例
let lang = type === Type.Strong ? new Java() : new JavaScript();
// 增加一个功能,即:创建实例后,运行该实例打印的方式
// 以下提示报错(lang 是 Java 和 JavaScript 的联合类型)此时 TS 是不能判断它具体是哪一种类型的
// 需要添加 类型断言,就不会报错了
// if(lang.helloJava){
// lang.helloJava()
// } else {
// lang.helloJavaScript()
// }
if ((lang as Java).helloJava) {
(lang as Java).helloJava();
} else {
(lang as JavaScript).helloJavaScript();
}
return lang;
}
getLanguage(Type.Strong);
注:
由于不知道程序在运行时,到底会传入什么样的参数,所以就必须要在每一处都加上类型断言。显然这不是一种理想的方案,代码的可读性很差
而类型保护机制就是用来解决这个问题的,它可以提前对类型做出预判。
# 1、什么是类型保护
TIP
- TypeScript 能够在特定的区块中保证变量属于某种确定的类型。
- 可以在此区块中放心地引用此类型的属性,或者调用此类型的方法。
接下来我们就来使用 4 种创建特殊区块的方法
# 2、instanceof 方法
TIP
该方法类型保护的方法用来判断一个实例是不是属于某个类
enum Type {
Strong,
week,
}
// 定义 Java 类
class Java {
helloJava() {
console.log("Hello Java");
}
}
// 定义 JavaScript 类
class JavaScript {
helloJavaScript() {
console.log("Hello JavaScript");
}
}
// 定义一个函数
function getLanguage(type: Type) {
// 判断该类型是 强类型 则返回 Java 类的实例,否则 返回 JavaScript 类的实例
let lang = type === Type.Strong ? new Java() : new JavaScript();
// instanceof 方法用来判断一个实例是不是属于某个类
// 如果 lang 属于 Java 类
if (lang instanceof Java) {
// TS 就能保证 lang 一定是 Java 类的实例,相应的方法也会自动提示出来
lang.helloJava();
} else {
// 在 else 区块中,TS 就会判断 lang 对象一定是 JavaScript 的实例,相应的方法也会直接提示出来
lang.helloJavaScript();
}
// 这样的代码就比之前使用 类型断言简洁多了
return lang;
}
getLanguage(Type.Strong);
# 3、in 方法
TIP
in 关键字可以判断一个属性是不是属于某个对象
我们在以上代码 Java 和 JavaScript 类中,分别加入属性
enum Type {
Strong,
week,
}
// 定义 Java 类
class Java {
helloJava() {
console.log("Hello Java");
}
// 新增属性
java: any;
}
// 定义 JavaScript 类
class JavaScript {
helloJavaScript() {
console.log("Hello JavaScript");
}
// 新增属性
javascript: any;
}
// 定义一个函数
function getLanguage(type: Type) {
// 判断该类型是 强类型 则返回 Java 类的实例,否则 返回 JavaScript 类的实例
let lang = type === Type.Strong ? new Java() : new JavaScript();
// in 方法可以判断一个属性是不是属于某个对象
// 通过以上新增的两个属性来创建类型保护区块(if 和 else 两个区块)
if ("java" in lang) {
// 在第一个区块中,TS 会判断该对象一定是 Java 类的实例
lang.helloJava();
} else {
// 在第一个区块中,TS 会判断该对象一定是 JavaScript 类的实例
lang.helloJavaScript();
}
return lang;
}
getLanguage(Type.Strong);
# 4、typeof 方法
TIP
typeof 方法 ,用来判断一个基本的类型
给 getLanguage 函数增加一个参数 x 为联合类型
enum Type {
Strong,
week,
}
// 定义 Java 类
class Java {
helloJava() {
console.log("Hello Java");
}
// 新增属性
java: any;
}
// 定义 JavaScript 类
class JavaScript {
helloJavaScript() {
console.log("Hello JavaScript");
}
// 新增属性
javascript: any;
}
// 给函数增加一个参数 x
function getLanguage(type: Type, x: string | number) {
// 判断该类型是 强类型 则返回 Java 类的实例,否则 返回 JavaScript 类的实例
let lang = type === Type.Strong ? new Java() : new JavaScript();
// typeof 方法 ,用来判断一个基本的类型
// 用 typeof 来判断 x 的类型
if (typeof x === "string") {
// 该区块中 x 的类型就一定是 string 类型,x 就可以拥有 string的一些属性,如下
x.length;
} else {
// 该区块中 x 的类型就一定是 number 类型,x 就拥有了 number 的一些方法
x.toFixed(2);
}
return lang;
}
// getLanguage(Type.Strong)
# 5、通过创建类型保护函数来判断对象的类型
该函数需要自定义
enum Type {
Strong,
week,
}
// 定义 Java 类
class Java {
helloJava() {
console.log("Hello Java");
}
// 新增属性
java: any;
}
// 定义 JavaScript 类
class JavaScript {
helloJavaScript() {
console.log("Hello JavaScript");
}
// 新增属性
javascript: any;
}
// 自定义创建类型保护函数来判断对象的类型
// 该函数的返回值是一种特殊的返回值,叫做 "类型谓词"
function isJava(lang: Java | JavaScript): lang is Java {
return (lang as Java).helloJava !== undefined;
}
// 给函数增加一个参数 x
function getLanguage(type: Type, x: string | number) {
// 判断该类型是 强类型 则返回 Java 类的实例,否则 返回 JavaScript 类的实例
let lang = type === Type.Strong ? new Java() : new JavaScript();
// 使用自定义类型保护函数来判断
if (isJava(lang)) {
// 该类型保护区块,TS 会判断该对象一定是 Java 类的实例
lang.helloJava();
} else {
lang.helloJavaScript();
}
return lang;
}
// getLanguage(Type.Strong)
# 6、总结
TIP
我们学习了 TS 的类型保护机制,分别是
- 类型推断
- 类型兼容性
- 类型保护
利用这些机制,再配合 IDE 的自定补全 和 提示功能,可以极大提高开发效率,可以深加利用。
# 四、TS 的高级类型
TIP
所谓 "高级类型",即:TS 为了保证语言的灵活性所引入的一些语言特性。这些特性将有助于我们应对复杂多变的开发场景。
我们先来学习 交叉类型 和 联合类型
# 1、交叉类型
TIP
交叉类型即:将多个类型合并为一个类型(使用 & 符号连接),新的类型将具有所有类型的特性。
因此,交叉类型特别适合对象混入的场景
// 定义一个 DogInterface 接口 ,具有 run() 方法
interface DogInterface {
run(): void;
}
// 定义一个 CatInterface 接口,具有 jump() 方法
interface CatInterface {
jump(): void;
}
// 定义变量 pet 的类型是 以上两个接口的交叉类型(交叉类型用 & 符来链接)
let pet: DogInterface & CatInterface = {
// 该变量同时具备以上两个接口的所有方法
run() {},
jump() {},
};
注:
从名称上看,交叉类型给人感觉是取类型的交集,但实际上是取所有类型的并集。
这里需要注意,不要混淆了。
# 2、联合类型
TIP
在前面的学习中,多少接触过一些。这里我们正式明确它的概念
所谓联合类型:声明的类型并不确定,可以为多个类型中的一个。
// 声明变量 a,它的类型是 number 和 string 的联合类型
// 它的取值既可以是数字类型 1 也可以是字符串类型 'a'
let a: number | string = 1;
// let a: number | string = 'a'
# 2.1、字面量联合类型
TIP
有时候我们不仅要限定一个变量的类型,而且还需要限定变量的取值在某一个特定的范围内
// 定义一个变量 b,它的类型是一个字符串类型的字面量联合类型
// 即:b 的取值只能是 'b','a','c' 中的一种
let b: "a" | "b" | "c";
// 数字类型的字面量联合类型,它的取值限定为只能是 1,2,3
let c: 1 | 2 | 3;
# 2.2、对象的联合类型
// 定义一个 DogInterface 接口 ,具有 run() 方法
interface DogInterface {
run(): void;
}
// 定义一个 CatInterface 接口,具有 jump() 方法
interface CatInterface {
jump(): void;
}
// 对象的联合类型
// 创建 Dog 类型,实现了 DogInterface 接口
class Dog implements DogInterface {
// 实现了 run 和 eat 方法
run() {}
eat() {}
}
// 创建 Cat 类型,实现了 CatInterface 接口
class Cat implements CatInterface {
jump() {}
eat() {}
}
enum Master {
Boy,
Girl,
}
// 定义一个函数
function getPet(master: Master) {
// 判断参数类型,如果是 Boy 即返回 Dog 的实例,否则返回 Cat 的实例
let pet = master === Master.Boy ? new Dog() : new Cat();
// 调用实例的方式,首先看下 pet 的类型被推断为 Dog 和 Cat 的联合类型
// 如果一个对象是联合类型,在类型未确定的情况下,它就只能访问所有类型的共有成员,Dog 和 Cat的共有成员 就是 eat 方法
// 如果想要访问其中的 run 方法,是不能访问的,同样 jump 也不能访问
pet.eat();
return pet;
}
注:
以上代码中,有趣的事情再次发生了,从名称上看联合类型给人的感觉是 取所有类型的并集,而实际上在对象的联合类型情况下,却只能访问所有类成员的交集。
接下来了解一种模式,该模式叫做:可区分的联合类型
# 2.3、可区分的联合类型
TIP
可区分的联合类型该模式本质上是结合了联合类型 和 字面量类型的一种 类型保护方法
它的核心思想是:一个类型如果是多个类型的联合类型 并且 每个类型之间有一个公共的属性,那么我们就可以凭借这个公共属性创建不同的类型保护区块。
// 声明两个接口 Square 和 Rectangle,它们有一个共同的属性 kind 用来表示它们的类型
// 声明接口 Square
interface Square {
kind: "square";
size: number;
}
// 声明接口 Rectangle
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
// 使用类型别名声明了一种 Square 和 Rectangle 的联合类型 Shape
type Shape = Square | Rectangle;
// area 函数,用来计算不同类型的面积
function area(s: Shape) {
// 通过两个接口共用的属性 kind,就可以创建不同的类型保护区块
switch (s.kind) {
case "square":
// 该区块在可访问 Square 的属性
return s.size * s.size;
case "rectangle":
// 该区块在可访问 Rectangle 的属性
return s.height * s.width;
}
}
注:
该模式的核心:即利用两种类型的共有属性来创建不同的类型保护区块
以上代码不升级是么有问题的,但如果我们想加一种新的 Shape 就有可能存在一种新的隐患
如果我们给以上代码添加一种新的 Shape(形状)类型
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
// 添加一种新的 Shape(形状)类型
interface Circle {
kind: "circle";
// 半径
r: number;
}
// 在联合类型中添加 Circle 类型
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
}
}
// 打印面积
console.log(area({ kind: "circle", r: 1 })); // undefined
注:
ts-node 文件名
命令行运行以上代码,打印结果为 undefined ,如何运用 TS 来约束这种模式了(即:给出对应的错误提示)。
有两种方法,如下
方法一: 为 area 函数指定明确的返回值类型
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
r: number;
}
type Shape = Square | Rectangle | Circle;
// 为 area 函数指定明确的返回值类型
// 如果指定为 number 类型(number处会报错),此时 TS 就会判断 所有的 switch 分支是不是包含了所有的情况
function area(s: Shape): number {
switch (kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
}
}
console.log(area({ kind: "circle", r: 1 }));
方法二: 利用 never 类型
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
r: number;
}
type Shape = Square | Rectangle | Circle;
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.height * s.width;
// 补上 circle 分支
case "circle":
return Math.PI * s.r ** 2;
default:
// 函数的参数是 area 的参数 s,此时 s 处会报错
// 该函数的作用是 检查 s 是不是 never 类型,如果 s 是 never 类型就说明前面的所有分支都被覆盖了,以下分支永远不会走到
// 如果 s 不是 never 类型,说明前面的分支有遗漏,那么再补上分支
return ((e: never) => {
throw new Error(e);
})(s);
}
}
// 打印面积(可正常输出了)
console.log(area({ kind: "circle", r: 1 })); // 3.141592653589793
# 3、总结
TIP
我们以上学习了 TS 的高级类型交叉类型 和 联合类型
- 交叉类型比较适合做对象的混入
- 联合类型可以使类型具有一定的不确定性,可以增强代码的灵活性
# 4、索引类型
TIP
在 JS 中我们经常会遇到以下的场景:从对象中获取一些属性的值,然后建立一个集合
let obj = {
a: 1,
b: 2,
c: 3,
};
// 需求:我们想抽取它的一些值,形成一个数组
/**
* 实现一个函数
* @param obj 对象
* @param keys 字符串数组
* @returns 数组
*/
function getValues(obj: any, keys: string[]) {
// 遍历 keys 数组,调用它的 map 方法
return keys.map((key) => obj[key]);
}
// 先抽取它的属性 a 和 b
console.log(getValues(obj, ["a", "b"])); // [ 1, 2 ]
// 再抽取 obj 中没有的属性 e 和 f
console.log(getValues(obj, ["e", "f"])); // [ undefined, undefined ]
注:
在以上抽取 obj 中没有的属性 e 和 f 时,TS 编译器并没有报错。如何使用 TS 对这种模式进行类型约束呢 ? 这里就会用到索引类型
要使用索引类型,得先了解索引类型的查询操作符
# 4.1、索引类型的查询操作符 - keyof T
TIP
keyof T
表示类型 T 的所有公共属性的字面量的联合类型
// keyof T
// 定义一个接口,有属性 a 和 b
interface Obj {
a: number;
b: string;
}
// 定义一个变量,它的类型为 keyof Obj
// key 的类型就变成了 a 和 b 的联合类型 let key: "a" | "b"
let key: keyof Obj;
# 4.2、索引访问操作符
TIP
索引访问操作符的语法:T[K]
,它的含义表示对象 T 的属性 K 所代表的类型
// T[K](索引访问操作符)它的含义表示对象 T 的属性 K 所代表的类型
// 定义一个变量 value,它的类型就是一个索引访问操作符
// 这里指定 Obj 属性 a 所代表的类型,即:value 的类型就是 number
let value: Obj["a"];
# 4.3、泛型约束
TIP
T extends U
表示泛型变量可以通过继承某个类型获得某些属性
清楚了 索引类型的查询操作符 keyof T
,索引访问操作符 T[K]
,泛型约束T extends U
这三个概念,我们就来改造以上的 getValues()
函数
let obj = {
a: 1,
b: 2,
c: 3,
};
// 改造前
// function getValues(obj: any, keys: string[]){
// return keys.map(key => obj[key])
//}
// 改造成泛型函数,需要做一些约束
// kyes 其中的元素一定是 obj 的属性
// 先给 getValues 定义一个泛型变量 T,用它来约束 obj
// 然后,再给 getValues 定义一个泛型变量 K,用它来约束 keys 数组
// 将 getValues<T, K> 其中的 K 增加类型约束,让它来继承 obj 所有属性的联合类型
// 函数的返回值首先它是一个数组,数组的元素的类型就是属性 K 对应的类型
// 这样我们就通过一个索引类型把一个 getValues 函数改造完毕了,这是 TS 的类型检查就发挥作用了
function getValues<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
return keys.map((key) => obj[key]);
}
console.log(getValues(obj, ["a", "b"])); // [ 1, 2 ]
// 如果指定了一个不在 obj 里的属性,编译器就会报错,由此可以看到索引类型可以实现对对象属性的查询和访问,然后在配合泛型约束就能够使我们建立对象、对象属性 以及 属性值之间的约束关系
console.log(getValues(obj, ["e", "f"]));
# 5、映射类型
TIP
通过映射类型我们可以从一个旧的类型生成一个新的类型
# 5.1、Readonly 只读 - 映射类型
TIP
比如:把一个类型中的所有属性变为只读
// 定义一个 Obj 接口
interface Obj {
a: string;
b: number;
c: boolean;
}
// 需求:将接口的所有属性变为 只读
// 定义一个类型别名
// 该类型别名是 TS 一个内置的泛型接口,接口的名称是 Readonly
// 该接口传入的类型是我们指定的 Obj,当鼠标划在 ReadonlyObj 上可以看到
// 这样新生成的类型 和 旧的类型,可以看到它们的成员是完全相同的,且每一个成员的属性都变成了只读
type ReadonlyObj = Readonly<Obj>;
它是如何实现的呢 ?按住 Ctrl 键,点击 Readonly 查看 TS 源码的实现方法,直接跳转至 TS 内置的类库中,该类库的位置在 \node_modules\typescript\lib
目录下,这里预定义了很多 TS 内置的类库。
注:
观察 Readonly 的实现
- Readonly 是一个泛型接口,而且是一个可索引类型的泛型接口
- 它的索引签名是
P in keyof T
其中keyof T
是一个索引类型的查询操作符,它表示类型 T 所有属性的联合类型 P in
相当于执行了一次for in
操作,它会把变量 P 依次的绑定到 T 的所有的属性上- 索引签名的返回值就是一个索引访问操作符了,即
T[P]
,这里代表属性 P 所指定的类型 - 最后再加上 readonly 就把所有的属性变成了只读
以上就是 Readonly 的实现原理了,TS 还预置了很多的 映射类型
# 5.2、Partial 可选 - 映射类型
TIP
把一个接口的所有属性变为可选的
// 定义一个 Obj 接口
interface Obj {
a: string;
b: number;
c: boolean;
}
// 需求:把一个接口的所有属性变为可选的
// 给类型别名 PartialObj 指定一个 TS 内置的映射类型叫 Partial,参数为 Obj
// 鼠标划在 PartialObj 上可以看到新的类型已经把所有属性 变成了可选
type PartialObj = Partial<Obj>;
注:
我们可以看到新的类型已经把所有的属性变成了可选,按住 Ctrl 键,点击 Partial 查看 TS 源码的实现方法
与以上只读的实现几乎是一致的,只不过把属性变成了可选
# 5.3、Pick 挑选- 映射类型
TIP
Pick 映射类型可以抽取 Obj 的一些子集
// 定义一个 Obj 接口
interface Obj {
a: string;
b: number;
c: boolean;
}
// 抽取 Obj 的一些子集
// Pick 接收两个参数:第一个是 Obj,第二是我们要抽取的一些属性的 key ,我们指定为 a 和 b
type PickObj = Pick<Obj, "a" | "b">;
以上 a 和 b 就会被单独的抽取出来,形成一个新的类型
按住 Ctrl 键,点击 Pick 查看 TS 源码的实现方法 和 原理
注:
- Pick 有两个参数,第一个参数是 T ,它代表我们要抽取的一个对象
- 第二个参数是 K,它有一个约束即:K 一定要来自 T 所有属性字面量的联合类型,新的类型的属性一定要从 K 中选取
以上 Readonly、Partial、Pick 三种类型官方将它们称为 同态,即:它们只会作用为 Obj 属性,而不会引入新的属性。
# 5.4、Record - 映射类型
TIP
Record 会创建新的属性
// 定义一个 Obj 接口
interface Obj {
a: string;
b: number;
c: boolean;
}
// Record 会创建新的属性
// Record 接收两个参数,第一参数是 预定义的新的属性 x 和 y ,该属性不来自 Obj
// 第二个参数是 一个已知的类型
type RecordObj = Record<"x" | "y", Obj>;
注:
鼠标划在 RecordObj 上可以看到,新的类型会有一些属性,这些属性有 Record 的第一参数所指定,这些属性的类型就是一个 已知的类型。
这种类型就是一个 非同态的类型 ,映射类型本质上一种预先定义的泛型接口,通常还会结合索引类型获取对象的属性 和 属性值,从而将一个对象映射成我们想要的结构。
# 5.5、总结
TIP
以上我们学习 TS 的映射类型,TS 预设了很多的映射类型,我们感兴趣可以去它的类库中来学习
# 6、条件类型
TIP
条件类型是一种由条件表达式所决定的类型
条件表达式的语法
T extends U ? X : Y
具体含义:
如果类型 T 可以被赋值给 类型 U ,结果类型为 X 类型,否则就是 Y 类型
条件类型使类型具有了不唯一性,同样也增加了语言的灵活性
// 条件表达式的语法 T extends U ? X : Y
// 定义一个类型别名 TypeName,它是一种条件类型
// 而且是一种条件类型的嵌套,它会依次判断 T 的类型,然后返回不同的字符串
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: T extends undefined
? "undefined"
: T extends Function
? "function"
: "object";
// 定义一个类型 T1,它是条件类型传入一个参数 string
// T1 的类型(鼠标划上去可看出)根据上面的逻辑,它就是一个字面量类型 string
type T1 = TypeName<string>;
// T2 的类型是一个字面量类型,值是 Object
type T2 = TypeName<string[]>;
# 6.1、分布式条件类型
TIP
分布式条件类型的含义:如果 T extends U ? X : Y
中类型 T 是一个联合类型的情况下,如:
(A | B) extends U ? X : Y
,即:T 是一个类型 A 和 类型 B 的联合类型,这时的结果类型会变成多个条件类型的联合类型,它可以进行如下拆解
(A extends U ? X : Y) | (B extends U ? X : Y)
type TypeName<T> = T extends string
? "string"
: T extends number
? "number"
: T extends boolean
? "boolean"
: T extends undefined
? "undefined"
: T extends Function
? "function"
: "object";
// 定义一个类型 T3 ,它的类型为 分布式的条件类型
// 此时,T3 的类型就会被推断为 string 和 object 的字面量联合类型
type T3 = TypeName<string | string[]>;
利用这个特性,可以帮助我们去实现一些类型的过滤
// 定义一个新的类型 Diff ,它又两个参数 一个是 T,一个是 U
// 具体实现:如果 T 可以被赋值给 U,结果类型就是 never 类型,否则就是 T 类型
type Diff<T, U> = T extends U ? never : T;
// 定义一个类型 T4
// Diff 的第一个参数传入abc的联合类型 "a" | "b" | "c" ,第二个参数 "a" | "e"
// T4 的类型就变成了 b 和 c 的联合类型,其中就过滤掉了第二个参数中已经含有的类型 a
type T4 = Diff<"a" | "b" | "c", "a" | "e">;
我们按照上边的拆解逻辑详细的解释
Diff 会被拆解为如下的样子
// Diff 会被拆解为多个条件类型的联合类型
// Diff<"a", "a" | "e"> | Diff<"b", "a" | "e"> | Diff<"c", "a" | "e">
// 判断 a 是不是可以被赋值给 字面量联合类型("a" | "e"),答案是可以的,所以它就会返回为 never
// b 和 c 是不可以被赋值给字面量联合类型("a" | "e") 因此,依然返回 b 和 c
// 因此,最终返回如下
// never | "b" | "c"
// 最后,never 和 b,c 的联合类型就是 "b" | "c"
// 通过分析,我们就可以看出 Diff 的作用即可从类型 T 中过滤掉可以赋值给类型 U 的类型
还可以基于 Diff
类型再做扩展,从类型中除去一些我们不需要的类型。如:undefined
和 null
type Diff<T, U> = T extends U ? never : T;
// 定义一个类型 NotNull,接收一个参数 T
// 然后,利用 Diff 从 T 中过滤掉 undefined 和 null
type NotNull<T> = Diff<T, undefined | null>;
// 定义一个 T5,NotNull 中传入一个联合类型
// 通过 NotNull 类型就可以过滤掉 undefined 和 null,T5 的类型就变为了 string 和 number(鼠标划上 T5 就能看到)
type T5 = NotNull<string | number | undefined | null>;
注:
实际上,我们上边实现了两个类型,官方已经为我们实现了,即:一些内置的类型
- Diff 的内置类型是
Exclude<T, U>
,从类型 T 中过滤掉可以赋值给类型 U 的类型,它的实现和我们上边的实现是一致的 - NotNull 的内置类型是
NonNullable<T>
在实际使用时,直接调用
Exclude<T, U>
和NonNullable<T>
这两个内置的类型即可
此外,官方还预制了一些条件类型,如:Extract<T, U>
,该类型与 Exclude<T, U>
相反
Extract<T, U>
可以从类型 T 中抽取出可以赋值给 U 的类型
// Extract 中有两个参数,第一个是 "a" | "b" | "c",第二个参数是 "a" | "e"
// 这样 T6 的类型就会变成 a,因为它抽取了在 U 中含有的类型 a
type T6 = Extract<"a" | "b" | "c", "a" | "e"
// >
# 6.2、ReturnType
TIP
ReturnType<T>
可以获取函数返回值的类型
// 定义一个类型 T7,ReturnType 的参数是一个函数,返回 string
// 此时 T7 的类型就是 string(鼠标划上去即可看到)
type T7 = ReturnType<() => string>;
按住 Ctrl 键,点击 ReturnType 查看 TS 源码的实现方法
源码实现解读
- ReturnType 要求参数 T 可以被赋值为一个函数,这个函数可以有任意的参数
- 返回值类型也是任意的(any)
- 由于返回值类型是不确定的,因此使用了一个 infer 关键字(它的作用表示待推断 或 延迟推断)需要根据实际的情况来确定
- 如果实际的情况是返回 R 结果类型就是 R ,否则返回值类型就是 any
以上就是 ReturnType 的实现原理 和 作用。当然 TS 的条件类型还有很多,感兴趣还可以通过 TS 的官方类库查阅 和 学习。
# 五、TS 核心基础知识总结
TIP
总结 TS 核心基础部分所学到的所有知识点
# 1、类型基础
语言类型 | 描述 | 举例 |
---|---|---|
强类型语言 | 不能改变变量的数据类型,除非进行强制类型转换 | Java、C#、Python、C/C++ |
弱类型语言 | 能够改变变量的数据类型 | JavaScript、PHP |
静态类型语言 | 在编译阶段确定变量的类型 | Java、C#、C/C++ |
动态类型语言 | 在执行阶段确定变量的类型 | JavaScript、PHP、Python |
# 2、基本类型
类型 | 描述 | |
---|---|---|
number | 数字 | / |
string | 字符串 | / |
boolean | 布尔 | / |
Array | 数组,type[] 、Array<type> | / |
Tuple | 元组,限定元素个数与类型的数组 | / |
function | 函数 | / |
object | 对象 | / |
symbol | 符号,独一无二的值 | / |
void | 没有类型 | / |
any | 任意类型 | / |
undefined | 未定义 | 任意类型的子类型 |
null | 没有值 | 任意类型的子类型 |
never | 永远不存在的值(抛异常、死循环) | 任意类型的子类型 |
# 3、枚举类型
类型 | 描述 |
---|---|
数字枚举 | 枚举成员值默认从 0 递增 |
字符串枚举 | 不支持反向映射 |
异构枚举 | 数字枚举 和 字符串枚举 混用,就构成了 异构枚举 (这种情况容易引起混淆。因此不建议使用) |
枚举成员 | 拥有只读属性 ①、 const member (在编译阶段被计算出结果),无初始值、对常量成员的引用、常量表达式②、 computed member (表达式保留到程序的执行阶段),非常量表达式 |
常量枚举 | 编译后被移除 成员只能为 const member |
枚举 / 枚举成员作为类型 | ①、无初始值 ②、枚举成员均为数字 ③、枚举成员均为字符串 |
# 4、接口
接口类型 | 描述 |
---|---|
对象类型接口 | 检查原则:鸭式辨型法 绕过对象字面量检查(将对象字面量赋值给变量、使用类型断言、使用字符串索引签名) 对象的属性(可选属性、只读属性) |
可索引类型接口 | 数字索引(相当于数组)[index: number] 字符串索引 [x: string] 以上两种混用时,数字索引签名的返回值必须是字符串索引签名返回值的子类型 |
函数类型接口 | interface A { (arg: type): type } |
混合类型接口 | interface B { (arg: type): type; prop: type; method(arg: type): type; } |
类类型接口 | 类必须实现接口中的所有属性 接口只能约束类的公有成员,不能约束私有成员、受保护成员、静态成员和构造函数 |
接口继承接口 | 抽离可重用的接口 将多个接口整合成一个接口 |
接口继承类 | 抽象出类的公有成员、私有成员和受保护成员 |
# 5、函数
分类 | 描述 |
---|---|
定义函数 | 定义方式(function、变量定义、类型别名、接口定义)注:定义函数类型,无函数体 类型要求:参数类型必须声明,返回值类型 ─ 般无需声明 |
函数参数 | ①、参数个数:实参和形参必须 — — 对应 ②、可选参数:必选参数不能位于可选参数后 ③、默认参数:在必选参数前,默认参数不可省略;在必选参数后,默认参数可以省略 ④、剩余参数 |
函数重载 | 静态类型语言:函数的名称相同,参数的个数或类型不同 TypeScript:预先定义一组名称相同,类型不同的函数声明,并在一个类型最宽松的版本中实现 |
# 6、类
选项 | 描述 |
---|---|
基本实现 | ①、类中定义的属性都是实例属性,类中定义的方法都是原型方法 ②、实例属性必须有初始值,或在构造函数中被赋值,或为可选成员 |
继承 | 子类的构造函数中必须包含 super 调用 |
成员修饰符 | ①、public:对所有人可见,所有成员默认为 public ②、private:只能在被定义的类中访问,不能通过实例或子类访问( private constructor :不能被实例化,不能被继承)③、protected:只能在被定义的类和子类中访问,不能通过实例访问( protected constructor :只能被实例化,不能被继承)④、readonly:必须有初始值,或在构造函数中被赋值 ⑤、static:只能由类名调用,不能通过实例访问,可继承 |
构造函数参数中的修饰符 | 将参数变为实例属性 |
抽象类 | 不能被实例化,只能被继承 ①、抽象方法包含具体实现:子类直接复用 ②、抽象方法不包含具体实现:子类必须实现 多态: 多个子类对父抽象类的方法有不同实现,实现运行时绑定 |
this 类型 | 实现实例方法的链式调用 在继承时,具有多态性,保持父子类之间接口调用的连贯性 |
# 7、泛型
类型 | 描述 |
---|---|
支持多种类型的方法 | ①、函数重载 ②、联合类型 ③、any 类型:丢失类型约束 ④、泛型:不预先确定的类型,使用时才确定 |
泛型函数 | 定义:function generic<T>(arg: T): T 调用: generic<type>(arg) 、generic(arg) 泛型函数类型: type Generic = <T>(arg: T) => T |
泛型接口 | 定义:interface Generic<T> {(arg: T): T } 实现: let generic: Generic<type> (必须指定类型) |
泛型类 | 定义:class Generic<T> {method(value: T) } 泛型不能应用于类的静态成员 实例化: let generic = new Generic<type>() let generic = new Generic() ,T 可为任意类型 |
泛型约束 | T extends U (T 必须具有 U 的属性) |
# 8、类型检查机制
分类 | 描述 |
---|---|
类型推断 | 含义:根据某些规则自动地为变量推断出类型 ①、基础类型推断:初始化变量、设置函数默认参数、确定函数返回值 ②、最佳通用类型推断:推断出一个可以兼容当前所有类型的通用类型 ③、上下文推断:根据事件绑定推断出事件类型 |
类型断言 | 含义:用自己声明的类型覆盖类型推断 方式: 表达式 as type , <type> 表达式 弊端:没有按照接口的约定赋值,不会报错 |
类型兼容性 | 含义:如果 X(目标类型) = Y(源类型),则 X 兼容 Y 接口兼容性、函数兼容性、枚举兼容性、类兼容性、泛型兼容性:(见下表) |
类型保护 | 含义:在特定的区块中保证变量属于某种确定的类型 创建区块的方法: ①、instanceof ②、typeof ③、in ④、类型保护函数,特殊的返回值: arg is type (类型谓词) |
类型兼容性
兼容性分类 | 描述 |
---|---|
接口兼容性 | 成员少的兼容成员多的(鸭式辨型法) |
函数兼容性 | ①、参数个数:目标函数多于源函数 可选参数和剩余参数,遵循原则:固定参数兼容可选参数和剩余参数、可选参数不兼容固定参数和剩余参数(严格模式)、剩余参数兼容固定参数和可选参数 ②、参数类型:必须匹配 参数对象:严格模式:成员多的兼容成员少的;非严格模式:相互兼容(函数参数双向协变) ③、返回值类型:目标函数必须与源函数相同,或为其子类型 |
枚举兼容性 | 枚举类型和数字类型相互兼容 枚举类型之间不兼容 |
类兼容性 | 静态成员和构造函数不在比较范围 两个类具有相同的实例成员,它们的实例相互兼容 类中包含私有成员或受保护成员,只有父类和子类的实例相互兼容 |
泛型兼容性 | 泛型接口:只有类型参数 T 被接口成员使用时,才会影响兼容性 泛型函数:定义相同,没有指定类型参数时就兼容 |
# 9、高级类型
类型 | 描述 |
---|---|
交叉类型(类型并集) | 含义:将多个类型合并为一个类型,新的类型将具有所有类型的特性 应用场景:混入 |
联合类型(类型交集) | 含义:类型并不确定,可能为多个类型中的一个 应用场景:多类型支持 可区分的联合类型:结合联合类型和字面量类型的类型保护方法 |
字面量类型 | 字符串字面量 数字字面量 应用场景:限定变量取值范围 |
索引类型 | 要点: ①、 keyof T (索引查询操作符):类型 T 公共属性名的字面量联合类型②、 T[K] (索引访问操作符):对象 T 的属性 K 所代表的类型③、泛型约束 应用场景: 从一个对象中选取某些属性的值 |
映射类型 | 含义: 从旧类型创建出新类型 应用场景: Readonly<T> :将 T 的所有属性变为只读Partial<T> :将 T 的所有属性变为可选Pick<T, K> :选取以 K 为属性的对象 T 的子集Record<K, T> :创新属性为 K 的新对象,属性值的类型为 T同态: 只作用于 T 的属性 |
条件类型 | 含义:T extends U ? X : Y (如果类型 T 可以赋值给类型 U,那么结果类型就是 X,否则就是 Y)应用场景: ①、 Exclude<T, U> :从 T 中过滤掉可以赋值给 U 的类型②、 Extract<T, U> :从 T 中抽取出可以赋值给 U 的类型③、 NonNullable<T> :从 T 中除去 undefined 和 null④、 ReturnType<T> :获取函数的返回值类型 |
大厂最新技术学习分享群
微信扫一扫进群,获取资料
X