2023-06-15
TypeScript
00
请注意,本文编写于 252 天前,最后修改于 161 天前,其中某些信息可能已经过时。

目录

常见面试题
类型推论
类型断言
双层断言
类型断言的限制
对断言的理解
const断言
! 非空断言
类型保护
instanceof 类型保护
typeof 类型保护
in 类型保护
is 自定义保护
联合类型完整性检查
函数重载
定义
优势
构造器重载
方法重载
接口重载
类型兼容性
特殊类型的兼容性
函数类型的兼容性
参数数量
返回值类型
函数重载
枚举值类型兼容性
类的类型兼容性
泛型类型的兼容性
声明合并
函数的合并--重载
接口的合并
其他合并
一些关键字在TS和JS中有不同的含义
其他类型
this类型
object类型
参考资料

经过前两篇文章《TypeScript入门教程学习》《TypeScript类型进阶》两篇文章已掌握TS过半的知识点了。本篇文章将介绍TS中一些相对高级一些的类型,并对类型兼容性进行深入阐述,最后补充一些比较零碎的TS知识点,计划将除类型编程(包括映射类型和infer类型)以外的知识点都讲完。

常见面试题

  • 说说你对类型断言的理解,断言与赋值的区别?
  • 为什么TypeScript选择结构性类型子系统?
  • 你如何理解函数重载?
  • 有哪些类型可以声明合并?

带着问题开始本篇文章的阅读吧

类型推论

我们知道即使不显式的定义类型,也能够自动做出类型推论,这背后就隐藏着TS的类型推论。

  • 不显示定义会进行类型推论
  • 声明未赋值推论为any
  • 根据上下文进行类型推论
ts
// 不显式的定义类型,也能够自动做出类型推论 let age = 1 // let age: number // 声明未赋值 自动推断为any类型 let person; // let person: any var arr = ['zhangsan', 13]; // arr: (number| string)[] // 上下文类型 (开启 strict模式) // mouseEvent 自动推论为 MouseEvent window.onmousedown = function(mouseEvent) { console.log(mouseEvent.test); //<- Error };

类型断言

  • 语法 值 as 类型

  • 用途

    • 将联合类型断言为其中的一个类型
    • 将一个父类型断言为更加具体的子类型
      • unknow 必须配合 as 使用
    • 将任何一个类型断言为 any
      • 滥用any可能掩盖真正的类型错误
      • 一方面不能滥用 as any,另一方面也不要完全否定它的作用,需要平衡
    • any 断言为一个具体的类型
  • 类型断言在类型编程的一个小技巧

ts
interface A { name: string; age: number; sex: boolean; } interface B { age: number; } // 我想从A类型中踢出B类型的key怎么办? type MinusKey<A extends object, B extends object> = { [P in Exclude<keyof A, keyof B>]: A[P] } type AB = MinusKey<A, B>; // 还有 其他方案吗? type AB2 = { [P in keyof A as P extends keyof B ? never : P]: A[P] } // 如果我是想获得key类型的联合类型呢? type Val = { [P in keyof A]: P extends keyof B ? never : P }[keyof A]; // "name" | "sex"

双层断言

  • 任何类型都可以断言为anyany类型可以断言为任何类型
  • 双层断言十有八九意味着一个运行时错误,除非迫不得已,千万别使用断言

类型断言的限制

  • 如果两个类型有继承上的关系则可以将一个类型断言为另一个类型,如下都是体现本条规则
    • 联合类型断言为其中的一个类型
    • 父类型可以断言为子类型
ts
interface Ani { name: string; } interface Person extends Ani { age: number; } interface Student extends Person { school: string; } let ani: Ani = {name: '熊猫'} let stu: Student = { name: 'zs', age: 23, school: '清华' } let stu2: Student = ani as Student; let ani2: Ani = stu as Ani;
  • 继承并非是严格意义上的继承,TS是结构型系统,这个后面再阐述。

对断言的理解

  • 类型断言 vs 类型转换
    • 类型断言只会影响 TypeScript 编译时的类型,类型断言语句在编译结果中会被删除
  • 类型断言 vs 类型声明 (断言与赋值的区别)
    • 赋值更严格,不能将父类赋值给子类,只能将子类赋值给父类
  • 类型断言也可以借助泛型实现
ts
function getCacheData<T>(key: string): T { return (window as any).cache[key]; } interface Cat { name: string; run(): void; } const tom = getCacheData<Cat>('tom'); // tom: Cat

const断言

const 断言是 TypeScript 3.4 中引入的一个实用功能。在 TypeScript 中使用 as const 时,可以将对象的属性或数组的元素设置为只读,向语言表明表达式中的类型不会被扩大。

ts
// const arr = [3, 4]; // arr: number[] const arr = [3, 4] as const; // arr: readonly [3, 4] // arr.push(5) // TS报错,不允许

在类型编程中,const的使用还会起到神奇的作用,比如

image.png 由于这个案例比较复杂,感兴趣的还是看原文吧 《一道3层的TypeScript面试题》

! 非空断言

  • 感叹号运算符称为非空断言运算符,添加此运算符会使编译器忽略undefinednull类型
  • 使用非空断言要非常小心
  • 非空断言常用语搜索内容
ts
const prepareValue = (value?: string) => { // ... parseValue(value!); // 使用非空断言要非常小心 }; // 非空断言常用于搜索内容 const arr = ['zs', 'ls']; const item = arr.find(it => it === 'ls'); // item: string | undefined const item2 = arr.find(it => it === 'ls')!; // item2: string

类型保护

  • 类型保护实际上是一种错误提示机制(运行时检查)
  • 主要思想是尝试检验属性、方法或原型、以确定如何处理值

instanceof 类型保护

ts
class Class1 { public age = 18; constructor() {} } class Class2 { public name = "TypeScript"; constructor() {} } function logClassInfo(inst: Class1 | Class2) { if (typeof (inst as Class1).age === 'number') { console.log((inst as Class1).age); } else { // console.log(inst.name); // 报错不能确定inst是 Class2类型 console.log((inst as Class2).name); } } // 上述案例比较冗肿, 使用 instanceof 类型保护优化 function logClassInfo2(inst: Class1 | Class2) { if (inst instanceof Class1) { console.log(inst.age); } else { // 排除了 Class1 只剩下 Class2 console.log(inst.name); } }

typeof 类型保护

typeof能确定除null以外的基本类型和function类型

ts
function test(x: string | number) { if (typeof x == "string") { // number类型没有length属性,这里已经识别x是string类型 return x.length + " is a str"; } if (typeof x == "number") { return x * 10; // 这里正常,说明x已被识别为number类型 } }

in 类型保护

ts
interface Ani { name: string } interface Person extends Ani { family: string[] } function getMember(obj: Ani | Person) { if('family' in obj) { // 如果前面没有in 后面一行会提示TS错误 return obj.family.join(', '); } return obj.name }

is 自定义保护

ts
function getVal(arg: number | 'name' | 'age') { if (['name', 'age'].includes(arg as string)) { console.log(arg.length); // TS报错 } else { console.log(arg.toFixed());// TS报错 } } function isString(value: number | 'name' | 'age'): value is 'name' | 'age' { return ['name', 'age'].includes(value as string); } function getVal2(arg: number | 'name' | 'age') { if (isString(arg)) { console.log(arg.length); // ok } else { console.log(arg.toFixed()); // ok } }

联合类型完整性检查

这个小知识点,不知道放哪里合适,暂且放到这里吧

  • 当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。
  • 比如,如果我们添加了 TriangleShape,我们同时还需要更新 area函数。
ts
const enum ShapeKind { Circle, Square, } interface Square { kind: ShapeKind.Square; size: number; } interface Circle { kind: ShapeKind.Circle; radius: number; } type Shape = Square | Circle; function area(s: Shape): number { // switch语句不能少,否则这一行TS报错 switch (s.kind) { case ShapeKind.Square: return s.size * s.size; case ShapeKind.Circle: return Math.PI * s.radius ** 2; } } const re = area({kind: ShapeKind.Circle, radius: 2})

函数重载

前面TS入门一文简单介绍过函数重载,这里再深入研究下

定义

  • 模糊定义: 一组具有相同名称不同参数列表和返回值无关的函数
  • 完整定义:包含以下规则的一组函数就是TS函数重载 (面试中回答前四条加粗部分内容他即可)
    1. 由一个实现签名 + 一个或多个重载签名合成
    2. 外部调用函数重载定义的函数时,只能调用重载签名,不能调用实现签名,这看似矛盾的规则,则其实是TS的规定:实现签名下的函数题都是给重载签名编写的,实现签名只是在定义时起到了统领所有重载签名的作用,在执行调用是就看不到实现签名了。
    3. 调用重载函数时,会根据传递的参数来确定你调用的事哪一个函数
    4. 只有一个函数体,只有实现签名配备了函数体,所有的重载签名都只是签名,没有配备函数体
    5. 关于参数类型规则
      1. 无论重载签名有几个参数,参数类型是何种类型,实现签名都可以是一个无参函数签名;
      2. 实现签名参数个数可以少于重载签名的参数个数
      3. 但实现签名如果准备包含重载签名的某个位置的参数,那实现签名就必须兼容所有重载签名该位置的参数类型【联合类型或者any 或unknown类型的一种】
    6. 关于重载签名和实现签名的返回值类型规则
      1. 必须给重载签名提供返回值类型,TS无法默认推导
      2. 提供给重载签名的返回值类型不一定为其执行时的真实返回值类型,可以为重载签名提供真实返回值类型,也可以提供void或unknow或any类型,如果重载签名的返回值类型是void或unknow或any类型,那么将有实现签名来决定重载签名执行时的真实返回值类型。当然为了调用时能有自动提示+可读性更好+避免可能出现了类型强制转换,强烈建议为重载签名提供真实返回值类型
      3. 不管重载签名的返回值类型是何种类型,实现签名都可以返回any类型或unknow类型,当然一般我们两者都不选择,让TS默认为实现签名自动推导返回值类型。
ts
// 重载签名 function sum(a: number, b:number): number; function sum(a: string, b:string): string; // 实现签名 function sum(a: number | string, b: number | string): number| string { if(typeof a === 'string') { return a + b } return a + (b as typeof a); } let result = sum(1, 3); // result: number let result2 = sum('b', 'c'); // result2: string sum('b', 2); // 报错 没有对应的重载签名

优势

函数重载或方法重载有以下几个优势

  1. 结构分明 让代码可读性可维护性提升许多,而且代码更漂亮
  2. 各司其职,自动提示方法和属性 每个重载签名函数完成各自功能,输出取值时不用前置转换就能出现自动提示,从而提高开发效率
  3. 更利于功能扩展

构造器重载

  • 构造器有返回值吗?
    • 尽管TS类构造器会隐式返回this,如果非要返回一个值,构造器只允许返回this, 但构造器不返回值也能通过编译,更没有返回值类型之说,从这个意义上,TS构造器可以说成是没有返回值这一说的构造函数(注意:TS构造器和JS构造器的返回值的说法不完全相同)
  • 构造器重载的意义
    • 构造器重载和函数重载基本相同,主要区别是:TS类构造器重载签名和实现签名都不需要管理返回值,TS构在器是在对象创建出来之后,但是还没有赋值给对象变量之前被执行,一般用来给对象属性赋值
  • 场景, 比如计算矩形的面积, 可以传入宽高两个参数,也可以传入一个对象

方法重载

与函数重载类似

接口重载

与函数重载类似

类型兼容性

所谓的类型兼容性用于确定一个类型是否能赋值给其他类型TypeScript中的类型兼容性是基于结构类型的,其基本原则是,如果x要兼容y,那么y至少要具有与x相同的属性。

ts
interface Person { name: string; } interface Animal { name: string; } let animal: Animal = { name: '熊猫' }; let person: Person = animal; // 允许这样赋值,因为TS是结构性类型子系统
  • 关于类型系统有两种设计方案
    • JavaC 使用基于名义类型的系统 (也叫类类型),严格的检测继承关系
    • TypeScript 使用基于结构性的类型系统 ,也被称为“鸭子类型”,像鸭子一样会游泳会“嘎嘎叫”的动物就是鸭子
  • 为什么javaScript选择结构性类型子系统?
    • 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。

特殊类型的兼容性

  • any类型可以赋值给除了never任意其它类型
    • 反过来其它类型也可以赋值给any
  • never类型可以赋值给其它任意类型,但其它类型不能赋值给never
  • unknownever同为顶级类型,其它类型都可以赋值给unknow类型,但unknow赋值给其它类型(不包括any)时要断言为具体类型

函数类型的兼容性

以 函数fa和函数fb举例, 如果想让fb = fa成立。

参数数量

  • 参数数量 fa的参数个数小于等于fb的参数个数
  • 参数类型 fb的参数类型与fa要对应 (同一个参数前者参数类型是后者的子集也可以)
  • 推理: 先不考虑返回值,无参数函数可以赋值给任意函数
  • 可选参数,有就校验这个参数类型, 没有就不校验这个参数
  • 剩余参数,等同于无数个可选参数
ts
// fa: (a:number) => number; let fa = (a: number) => 0; // fb: (b: number, c: string) => number let fb = (b: number, c: string) => 0; // 如果将函数fb赋值给函数fa,那么要求fa中的每个参数都应该在fb中有对应,也就是fa的参数个数小于等于fb的参数个数 fb = fa; // 👌 fa = fb; // not ok 函数fb的参数数量大于fa函数的参数数量 // fb2的参数类型与fb要对应 (同一个参数前者参数类型是后者的子集也可以) let fb2 = (b: 2 | 3, c: string) => 0; fb = fb2; // 按照前个参数总结, 先不考虑返回值,无参数的函数可以赋值给任意函数 fb = () => 0; // 可选参数, 如果有就校验参数类型, 没有就不校验这个参数 let fc:(arg: number, arg2?: number) => number = (num1: number, num2: number) => 0 // 剩余参数意味着是任意个可选参数, 校验同可选参数校验 let fd: (...args: number[]) => number; fd = (arg: number, arg2: number) => 0 fc = () => 0;
  • 函数的双向协变 (参数类型无需绝对相同,能断言成立就行)
ts
let funcA = function(arg: number | string): void {}; let funcB = function(arg: number): void {}; // funcA = funcB 和 funcB = funcA都可以

返回值类型

  • 返回值类型同参数类型校验一样,但是返回值不存在双向协变

函数重载

  • 带有重载的函数,要求被赋值的函数的每个重载都能在用来赋值的函数上找到对应的签名:(简单说就是依次检查重载签名是否兼容, 规则同上)
ts
function merge(arg1: number, arg2: number): number; // merge函数重载的一部分 function merge(arg1: string, arg2: string): string; // merge函数重载的一部分 function merge(arg1: any, arg2: any) { // merge函数实体 return arg1 + arg2; } function sum(arg1: number, arg2: number): number; // sum函数重载的一部分 function sum(arg1: any, arg2: any): any { // sum函数实体 return arg1 + arg2; } let func = merge; func = sum; // error 不能将类型“(arg1: number, arg2: number) => number”分配给类型“{ (arg1: number, arg2: number): number; (arg1: string, arg2: string): string; }”

枚举值类型兼容性

  • 数字枚举成员类型与数字类型是互相兼容的
ts
enum Status { On, Off, } let s = Status.On; s = 1; // ok s = 3; // error 只能是 1或2
  • 不同枚举类型的枚举值之间是不兼容的(即使数字值一样)
ts
enum Status { On, // Status.On = 0 } enum Color { White, // Color.White = 0 } let s = Status.On; s = Color.White; // 不能将类型“Color.White”分配给类型“Status”。
  • 字符串枚举成员类型和字符串类型是不兼容的
ts
enum Status { On = "on", Off = "off", } let s = Status.On; s = "off"; // error

类的类型兼容性

  • 比较两个类的类型兼容性时,只有实例的属性和方法会相比较,类的静态成员和方法及构造函数不进行比较
ts
class Animal {} class People { static age: number; constructor(public name: string) {} static getAge: () => number; sayHello(to: string) {} } class Food { constructor(public num: number, public name: string) {} howToCook(to: string) {} // sayHello(to: number) {} // 方法的检测与函数的检测规则一样 // sayHello(to: string) {} } let a: Animal = new Animal(); let p: People = new People('zs'); let f: Food = new Food(1, '米饭'); a = p; // ok 类的静态属性和方法及构造函数不会参与比较 a = f; // oK 类的实例属性和方法比较 遵循结构性继承关系, 即f是a的子类 p = f; // error f中没有sayHello方法
  • 类的私有成员和受保护成员会影响类的兼容性
ts
class Parent { private age: number; // protected age: number; // 换成protected也不行 } class Children extends Parent { } class Other { private age: number; } const children: Parent = new Children(); const other: Parent = new Other(); // 不能将类型“Other”分配给类型“Parent”。类型具有私有属性“age”的单独声明
  • 补充:通过typeof可以获得类的静态类型, 静态类型的比较与实例类型比较规则类似,此时会比较构造函数
ts
class Animal {} class People { static age: number; // constructor(public name: string) {} // 构造器也会参与比较 static getAge: () => number; sayHello(to: string) {} } var aniClass: typeof Animal = {} as any as typeof Animal; var peopClass: typeof People = {} as any as typeof People; aniClass = peopClass // ok peopClass = aniClass // error aniClass 没有静态属性age 和静态方法 getAge

泛型类型的兼容性

  • 泛型类型的兼容性会根据泛型的推断结果决定
ts
interface Data<T> {} let data1: Data<number> = { name: "zs" }; let data2: Data<string> = {}; data1 = data2; // ✅ 因为推断的结果结构一样 let dat1: Data2<number> = {data: 1}; let dat2: Data2<string> = {data: 'str'}; dat1 = dat2; // error 推断的结果不一样

声明合并

如果定义了两个相同名字的函数、接口或类,那么他们会合并成一个类型

函数的合并--重载

ts
function reverse(x: number): number; function reverse(x: string): string; function reverse(x: number | string): number | string { if (typeof x === 'number') { return Number(x.toString().split('').reverse().join('')); } else if (typeof x === 'string') { return x.split('').reverse().join(''); } }

接口的合并

  • 浅层合并
  • 类型不一致会报错
ts
interface Alarm { price: number; } interface Alarm { weight: number; } // 等价于 interface Alarm { price: number; weight: number; } interface Alarm { price: string; // 类型不一致,会报错 } interface Alarm { info: { a: number; } } interface Alarm { info: { b: string; // 报错 不存在深度合并 } }
  • 接口方法的合并与函数的合并一样

其他合并

  • 类的合并与接口合并一致
  • 命名空间也会合并 具体会在下一章节讲述

一些关键字在TS和JS中有不同的含义

关键字JS中含义TS中含义
void忽略返回值
或返回值是undefined
表示一种类型
undefined是它的子集
区别是在一些高级回调中兼容不同类型的转化
typeof判断基本类型和function类型,不包括null
具有类型保护的作用
获取JS对象的类型,获取类的类型
class变量声明的一种方式既是声明变量也是定义一种类型
instaceof一种运算符 检测左边是否是右边的实例具有类型保护的作用
in检测属性具有类型保护的作用
!转换为boolean非空断言
new使用构造函数创建对象定义构造函数类型

其他类型

this类型

  • 增加一个虚拟参数this来表示this的类型,这个虚拟参数放在函数参数的第一个位置。
  • this类型仅在类或接口的非静态成员中可用 (通过es5的方式定义类, TS的支持不友好)
  • this 用来给三方库的使用补充TS描述
  • class中构造器函数默认返回this
  • 配置文件开启--noImplicitThis 更精确的检查this
ts
// 1. 对于es5的通过function定义类TS支持不优好,这里不好举例 // 2. this 用来给三方库的使用补充TS描述 declare interface Lib { name: string; sayHello: (this: Lib) => Lib; } interface Window { myLib: Lib; } const resp = window.myLib.sayHello(); // 3 let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); // 这里的this 并不是deck, 实际可能是 window(非严格模式)或者undefined(严格模式下) // 需要配置文件开启 -- noImplicitThis 才会提示错误 return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }, createCardPicker2: function(this: typeof deck) { return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } } } let cardPicker = deck.createCardPicker2(); let pickedCard = cardPicker();

如果您已经熟悉了this的指向(默认绑定、隐式绑定、显示绑定、new绑定)问题,那么TS中this没有什么知识点要说了。

object类型

  • object表示非原始类型,也就是除numberstringbooleansymbolnullundefined之外的类型。
  • 使用object类型,就可以更好的表示像Object.create这样的API。例如
ts
declare function create(o: object | null): void; create({ prop: 0 }); // OK create(null); // OK // create(42); // Error

参考资料

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!