• 【面试题】 TypeScript 前端面试题 由浅到深


    给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备            推荐:★★★★★

    地址:web前端面试题库

    基本类型介绍

    1.Boolean,Number,String

    声明:类型 = 类型对应变量

    1. let flag:boolean = true
    2. let age: number = 21;
    3. let name: string = "shixin";
    4. 复制代码

    类型收敛——字面量类型

    1. const flag: true = true;
    2. const age: 21 = 21;
    3. const name: 'shixin' = 'shixin';
    4. 复制代码

    2.undefined,null

    在 TypeScript 中,null 与 undefined 类型都是有具体意义的类型。所以在默认情况下会被视作其他类型的子类型。

    1. const data1: null = null;
    2. const data2: undefined = undefined;
    3. const data3: number = null; // That's OK
    4. const data4: string = null; // That's OK
    5. 复制代码

    tsconfig strictNullChecks 配置项,会严格校验undefined和null。在该配置开启的环境下。undefined和null将不为其他类型的子类型

    1. {
    2. "extends": "./tsconfig",
    3. "compilerOptions": {
    4. "strictNullChecks": true
    5. }
    6. }
    7. 复制代码
    1. const data5: string = null; // error
    2. 复制代码

    3.联合类型

    联合类型就是一个数组的可用的集合

    1. let numOrStr :string|number = 1;
    2. numOrStr = 'a'
    3. // ok
    4. 复制代码

    4.Array

    在 TypeScript 中有两种方式来声明一个数组类型:

    根据项目规范二选一(如果没有这个规范可以随便写)

    1. const arr1: string[] = []; // 可以向数组内添加string类型的值
    2. const arr2: Array = [];
    3. 复制代码

    类型收敛——元组

    场景1 基本类型元组定义

    或许我们想定义一个数组来描述三家公司的名称(只有那三家,没有第四家,也不会只有两家),公司名是string,长度是3。但是我们声明string[]就是有问题的,因为他不够收敛。这个时候我们就可以使用元组了。

    形式 [typeofmember1, typeofmember2,typeofmember3]

    元组声明:[string,string,string]

    1. const companyArr: [string,string,string] = ['公司n1', '公司n2', '公司n3'];
    2. 复制代码

    场景2 字面量类型元组定义

    或许我们想定义一个数组来描述三家优秀公司的名称(NIO,小鹏,理想) 元组声明:['NIO','小鹏','理想']

    1. const companyArr1: ['NIO','小鹏','理想'] = ['NIO','小鹏','理想'];
    2. 复制代码

    在这种情况下,对数组合法边界内的索引访问(即companyArr1[0],companyArr1[1],companyArr1[2])将能够安全的返回我们理想中的哪三颗韭菜。

    场景3 元组类型的 Rest 使用

    或许我们想定义一个数组来定义一个人前三项分别为姓名,年龄,性别,后面可以添加若干项描述。这时候我们就可以引入Rest,语法如同js一样,三个点。 元组声明:[string,number,string,...string[]]

    1. const Person: [string, number, string, ...string[]] = ['shixin', 22, '男', 'JV', '前端'];
    2. 复制代码

    但是问题来了, [string, number, string, ...string[]]未免太难理解了,我怎么知道哪一项代表了哪个信息。在TS4.0添加了具名元组。操作如下

    1. const Person: [name:string, age:number,sex:string,...desc:string[]] = ['shixin', 22, '男', 'JV', '前端'];
    2. 复制代码

    5.常用对象类型描述

    对于上面的元组场景3(定义一个数组来定义一个人前三项分别为姓名,年龄,性别)毕竟不是我们的常规操作,更加常见的是我们通过定义一个对象来完成对于一个人的定义。那么如何描述一个对象呢?

    内联注解

    我们可以如同基本类型一样直接在声明后面添加对象的类型描述

    1. const shixin: { name: string; age: number; sex: string; desc: string[] } = {
    2. name: 'shixin',
    3. age: 22,
    4. sex: '男',
    5. desc: ['JV', '前端']
    6. };
    7. 复制代码

    接口 interface

    或通过interface来声明一个类型变量,在变量内进行对象的类型描述

    1. interface IPerion {
    2. name: string;
    3. age: number;
    4. sex: string;
    5. desc: string[];
    6. }
    7. const shixin: IPerion = {
    8. name: 'shixin',
    9. age: 22,
    10. sex: '男',
    11. desc: ['JV', '前端']
    12. };
    13. 复制代码

    这样声明的好处,最直接的就是可以复用,这对于我们维护和统一类型起源非常有帮助。

    1. import { type IPerion } from 'xxx';
    2. const zhang3: IPerion = {
    3. name: 'zhang3',
    4. age: 20,
    5. sex: '男',
    6. desc: ['xx', 'xx']
    7. };
    8. 复制代码

    object、Object 以及 { }

    Object用于描述一个对象,就是万物皆为对象的那个对象。在js中他包含了原始的功能,比如谁都有的toString。也正因为如此,这个属性其实并不好用。甚至有点像any。

    1. let data1: Object = 'a'; // That's OK
    2. let data2: Object = 1;// That's OK
    3. let data3: Object = () => {};// That's OK
    4. 复制代码

    为了解决上面这个不好用的Object,在(typescript2.2)[www.typescriptlang.org/docs/handbo…] 引入了object type以表示不含原始类型(number,string,symbol等等)的object类型

    1. const data1:object = 42; // Error
    2. const data2:object = 'string'; // Error
    3. const data3:object = false; // Error
    4. const data4:object = undefined; // Error
    5. const data5:object = { prop: 0 }; // OK
    6. 复制代码

    {},一个空对象。你可以在该类型初始化的时候赋予各种各样的值,如同上面的第一个Object。但是在赋值的时候却比较麻烦。

    1. const data1: {} = { foo: 1 }; // OK
    2. data1.baz = 2;// Error
    3. 复制代码

    或许这样看起来并不常见,那么这个呢。

    1. const obj = {};
    2. 复制代码

    事实上,这种定义的方式会讲{}推导为变量obj的类型。在javascript时期,这行代码随处可见。以至于在ts的代码中,依然会有非常多的这种写法,从而出现了下面这些代码.

    1. const obj = {};
    2. // 添加一个属性
    3. obj.foo = 1; //类型“{}”上不存在属性“foo”。
    4. // ⬇
    5. /**
    6. * 给个初始值
    7. */
    8. const obj1 = {
    9. foo: undefined
    10. };
    11. obj1.foo = 1; //不能将类型“1”分配给类型“undefined”。
    12. // ⬇
    13. /**
    14. * 分支1:给个初始类型undefined
    15. */
    16. const obj: {
    17. foo: undefined | number;
    18. } = {
    19. foo: undefined
    20. };
    21. obj.foo = 1;
    22. /**
    23. * 分支2:把foo初始为-1
    24. */
    25. const obj = {
    26. foo: -1
    27. };
    28. obj.foo = 1;
    29. 复制代码

    除了object之外,其他两种方式其实在初始化变量的时候非常的开放,对于类型基本没有收敛。所以我们更建议不要使用。而object实际上对变量的描述并不够细致,通常对于对象的创建,我们都能够预想到对象的内容,所以我们更倾向于使用interface接口来定义对象。

    6.枚举

    枚举的作用和定义形式像一个简单的键值一一对应的map

    1. const CaseMap = {
    2. case1: 'Im case1',
    3. case2: 'Im case2'
    4. };
    5. enum CaseMap {
    6. case1 = 'Im case1',
    7. case2 = 'Im case2'
    8. }
    9. 复制代码

    如果你没有声明枚举的值,它会默认使用数字枚举,并且从 0 开始。

    1. const Direction = {
    2. Up: 0,
    3. Down: 1,
    4. Left: 2,
    5. Right: 3
    6. };
    7. enum Direction {
    8. Up,
    9. Down,
    10. Left,
    11. Right
    12. }
    13. 复制代码

    如果你希望数字枚举从 1 开始,只需要在第一项枚举值声明起始值,那么接下来的项都会递增。

    1. enum Direction {
    2. Up = 1,
    3. Down,
    4. Left,
    5. Right
    6. }
    7. 复制代码

    对于枚举和对象的区别,也很简单,枚举是双向的,对象是单向映射的。原理很简单,我们将上面Direction编译为js,编译产物如下。

    1. "use strict";
    2. var Direction;
    3. (function (Direction) {
    4. Direction[Direction["Up"] = 1] = "Up";
    5. Direction[Direction["Down"] = 2] = "Down";
    6. Direction[Direction["Left"] = 3] = "Left";
    7. Direction[Direction["Right"] = 4] = "Right";
    8. })(Direction || (Direction = {}));
    9. 复制代码

    给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

     地址:web前端面试题库

    7.any,unknow,never

    any,unknow

    any,unknow基本可以用于声明所有的类型。在某些时候我们是需要这种能力的。例如系统日志对于error的catch。我们无法知道用户会在里面报什么类型的错误,也不知道谁会在代码里面throw 一个number,string,boolean类型的Error出来。对于这种完全无法预知的变量,我们就可以定义为any或者unknow。

    那么都是可以用于声明任意类型,any和unknow的区别是什么呢。

    unknow对应中文就是不知道的意思。既然是不知道,即使定义的时候,我们无法确定类型,所以使用的时候,我们也不能随意使用。

    1. try{
    2. // xxxxx
    3. } catch (e) {
    4. const num: number = e; //不能将类型“unknown”分配给类型“number”。
    5. if (isNumber(e)) {
    6. const num1: number = e; // e is number
    7. }
    8. }
    9. 复制代码

    如上例子,我们需要通过一些判断手段,才能够对e进行使用。这样带来的好处就是,就算我不知道e是什么类型,但是只要通过一些必要的判断,我就能够安全的去操作他。坏处也显而易见,无论我做什么操作,我都需要进行判断,对类型进行过滤,才能够正常使用。

    而any在这个demo的表现就相对的肆无忌惮。

    1. try{
    2. // xxxxx
    3. } catch (e:any) {
    4. const num: number = e; //ok
    5. const str: string = e; // ok
    6. if (isNumber(e)) {
    7. const num1: number = e; // e is number
    8. }
    9. }
    10. 复制代码

    并且当我们定义了一个变量为any之后,他是具有传染性质的。any下面的所有属性都将是any。也就是在使用声明为any类型的变量时,该变量下面的所有属性都是危险的any。类型推断将会失效。

    1. const data: any = 1;
    2. 复制代码

    any用的很爽,但是背后的代价就是把ts变成了anyScript,抛弃所有的类型推断,以让你的代码编辑看起来没有任何报错,表面风平浪静,背后暗流涌动。所以在我们想声明一个未知的catch error以及其他不知道的变量的时候,或许使用unknow更好,而现在unknow也成为了catch error的默认类型。

    never

    never代表着走不到头,无法执行下去的类型。抛开在类型体操的使用,在日常声明的使用场景多为 throw error。

    1. function thouchError(): never {
    2. throw new Error();
    3. console.log("已经error咯,不能正常执行下面的内容了")
    4. }
    5. 复制代码

    在实际应用中,我们通过if ... else ...在最后的else添加never的声明。这时候类型报错就可以帮助你处理干净所有的类型情况。例子如下:

    1. const fun = (data: string | number) => {
    2. if (isString(data)) {
    3. data.charAt(1); //OK
    4. } else if (isNumber(data)) {
    5. data.toFixed(); //OK
    6. } else {
    7. const check: never = data;
    8. throw new Error(check);
    9. }
    10. };
    11. 复制代码

    当data的类型情况多了一个boolean,ts就会推断报错。

    1. const fun = (data: string | number | boolean) => {
    2. if (isString(data)) {
    3. data.charAt(1); //OK
    4. } else if (isNumber(data)) {
    5. data.toFixed(); //OK
    6. } else {
    7. const check: never = data; //不能将类型“boolean”分配给类型“never”。
    8. throw new Error(check);
    9. }
    10. };
    11. 复制代码

    TS基本心智

    类型上下文收敛和推断

    类型推断就是TypeScript会根据上下文代码自动帮我们推算出变量或方法的类型。

    当我们又一个函数名为 padLeft。如果参数 padding 是一个数字,我们就在 input 前面添加同等数量的空格,而如果 padding 是一个字符串,我们就直接添加到 input 前面。

    1. function padLeft(padding: number | string, input: string) {
    2. return new Array(padding + 1).join(' ') + input; //运算符“+”不能应用于类型“string | number”和“number”。
    3. }
    4. 复制代码

    报错的原因很简单,不能用string + 1(尽管在js中是可以的),而变量padding是个number或者string,这样运算最后可能不是我们想要的结果。那么我们可以这样做

    1. function padLeft(padding: number | string, input: string) {
    2. if (typeof padding === "number") {
    3. return new Array(padding + 1).join(" ") + input;
    4. }
    5. return padding + input;
    6. }
    7. 复制代码

    而在这个过程中,我们其实对变量进行了几次收敛和推断。

    1. 通过typeof number 讲padding收敛为number
    2. 推断new Array(padding + 1)推断为数组
    3. 通过join推断new Array(padding + 1).join(" ")为字符串
    4. 顺利的让new Array(padding + 1).join(" ")可以和input相加
    5. 如果能走到最后的return padding + input 则说明typeof number为false。那么padding就是string
    6. string的padding可以顺利的和input相加

    我们的类型系统就是如此保障我们的代码运行安全的。而对于类型上下文的收敛 ,这里借助(官方文档)[www.typescriptlang.org/docs/handbo…] 的demo举两个例子。

    • typeof
    1. function printAll(strs: string | string[] | null) {
    2. if (typeof strs === "object") {
    3. for (const s of strs) {
    4. // Object is possibly 'null'.
    5. console.log(s);
    6. }
    7. } else if (typeof strs === "string") {
    8. console.log(strs);
    9. } else {
    10. // do nothing
    11. }
    12. }
    13. 复制代码
    • in操作符
    1. type Fish = { swim: () => void };
    2. type Bird = { fly: () => void };
    3. function move(animal: Fish | Bird) {
    4. if ("swim" in animal) {
    5. return animal.swim();
    6. // (parameter) animal: Fish
    7. }
    8. return animal.fly();
    9. // (parameter) animal: Bird
    10. }
    11. 复制代码
    • 穷举检查(参考上面的never相关demo)

    对于这个类型的收敛,我们通常会用控制流分析来描述。通过上面的收敛方法,在if中进行对类型的控制,让一个变量可以被观察到不同的类型,作出不同的操作。除了上面的操作外,我们常常会自己去封装控制流的方法,也就是类型守卫。

    类型守卫

    为了拿到一个变量的string类型分支,我们常常会去做控制流分析。对于typeof string多次的使用,我们就会去封装一个isString的方法。以优雅我们的代码。但是当我们如下去完成一个isString。

    1. const isString = (val: unknown): boolean => typeof val === 'string'
    2. function padLeft(padding: number | string, input: string) {
    3. if (isString(padding)) {
    4. const str: string = padding; //不能将类型“string | number”分配给类型“string”。
    5. }
    6. // TODO xxx
    7. }
    8. 复制代码

    我们会发现竟然报错了,但从我们主观类型推断的角度这padding确实已经是个string了。其实在typescript中,控制流分析是无法跨越上下文的。也就是我们padLeft函数中,我们无法从isString这个方法中捕获到类型的形象,我们只能知道他返回的是个boolean。为了解决这类问题,TS引入了 is 关键字。

    语法: is 关键字 + 预期类型 我们只需要把 val is string替换掉boolean即可

    1. const isString = (val: unknown): val is string => typeof val === 'string'
    2. function padLeft(padding: number | string, input: string) {
    3. if (isString(padding)) {
    4. const str: string = padding; //不能将类型“string | number”分配给类型“string”。
    5. }
    6. // TODO xxx
    7. }
    8. 复制代码

    同样的,vue3向外抛出的 (isRef)[vuejs.org/api/reactiv…] 方法的原理实现也添加了类型守卫

    1. ///core/packages/reactivity/src/ref.ts
    2. export function isRef(r: Ref | unknown): r is Ref
    3. export function isRef(r: any): r is Ref {
    4. return !!(r && r.__v_isRef === true)
    5. }
    6. 复制代码

    类型先行

    当Typescript成为开发的附属品,javascripter会先将代码撸完,在开发过程中只写一些简单的类型声明。如果javascripter在逻辑编程上不关注类型编程。我们总能看到最后的代码成果会嵌套着大量的any , as等等不规范的操作。并不是any,as不能用。只是大多数人的使用场景,仅仅是因为改不动之前的数据结构了,或者同一个变量在许多的地方都有进行操作,而在操作的过程中,数据早就变了味(包括但是不限于可以往一个对象里来回赋值,string与number直接用+运算)。在这样的操作之后,类型系统将会变得更加的难以维护,甚至由于粗暴的as。让下一位开发者进入了一个对类型的错误判断,在这基础上进行了后续的操作。

    为避免这种现象,我们可以提前进行基础类型的完成。

    例如我们进行业务开发的时候的业务逻辑依赖于接口,我们就可以定义好接口的入参和出参数的interface。而这一份interface将成为我们对于这一块业务的开发基础类型。

    1. interface IUserReq{
    2. userId:number
    3. }
    4. interface IUserRsp{
    5. userId:number,
    6. userName:string,
    7. QRCode:string,
    8. age:number
    9. }
    10. 复制代码

    当我们将有如上的类型基准,那么接下来我们就可以从这一份类型基准,去进行开发,接下来的与user相关的代码类型,都可以是IUserRsp的衍生类型。

    例如我们进行业务开发的时候的业务逻辑依赖于数据库,我们就可以定义好数据库的类型。那么我们在node开发的过程中,相关数据库数据操作数据的代码类型,都可以是数据库的衍生类型。

    例如我们进行组件开发的时候,我们就可以定义好组件入参props的类型。那么我们在组件开发的过程中,相关props的组件变量类型,都可以是props的衍生类型。

    类型约束和维护

    不少开发者都称typescript是带着脚镣跳舞,脚镣就意味着约束。这句话的背后意思就是,我们在用TS的时候,其实是在一个约束的条件下,去完成我们的代码开发。而对于类型的约束,约束的程度就会直接反应到我们的项目代码上。

    上面我们讲到组件开发以props作为类型的源头,那么以vue3组件开发为例子,我们会在setup语法糖下使用defineProps宏进行props声明。

    1. const props = defineProps({
    2. title: {
    3. type: String,
    4. },
    5. UserList:{
    6. type:Array
    7. }
    8. });
    9. 复制代码

    但是在这个props宏中,我们的声明收敛的其实非常烂,对于UserList,如果数据源来自我们上面的接口,那么这个Array是完全不满足的,我们从UserList上拿到项后直接进行取值,是毫无类型提示的,因为UserList此时是一个Array。

    为了解决这个问题,vue3提供了PropType这个类型工具,以生成我们的props类型 。然后我们可以通过将type 通过as强行声明到我们通过PropType生成的props类型。也就是这样。

    1. const props = defineProps({
    2. title: {
    3. type: String,
    4. },
    5. UserList:{
    6. type:Array as PropType<User[]>
    7. }
    8. });
    9. 复制代码

    这样我们组件类型源头props才会被收敛,我们后面在基于props进行完成开发的时候,才会有类型保障。这就是类型收敛的意义。

    tips:defineProps在vue3+TS可以 仅声明类型,配合withDefaults宏就可以达到上面的效果

    1. export interface Props {
    2. title?: string;
    3. UserList?: User[];
    4. }
    5. const props = withDefaults(defineProps<Props>(), {
    6. title: '',
    7. UserList: () => []
    8. });
    9. 复制代码

    给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

     地址:web前端面试题库

    泛形

    上面我们通过PropType这个类型工具,生成了我们的props类型。而通过一个类型生成另一个目标类型,就是我们泛形的主要目的。以最简单的Array类型来说。我们就可以通过传入一个类型string,Array来生成一个string类型的数组。

    Array类比为js,他可能是这样一段代码

    1. function createArray(T) {
    2. const array = new Array()
    3. array.type = T;
    4. return array;
    5. }
    6. 复制代码

    泛形可以类比为函数,通过一个入参,再通过函数内部的逻辑改造,我们就可以得到一个返回值。比如最简单的add函数。类型的实现与函数的实现,其实在使用上和定义上基本是一致的。区别就在于在类型上完成两数的加法具体实现可能不是那么容易。

    1. const add = (num1, num2) => num1 + num2;
    2. const res = add(1,1); // res = 2
    3. type add = /**一系列类型操作 */;
    4. type res = add<1,2>; // type res = 3
    5. 复制代码

    于此相似的还有接口泛形,区别不大

    1. interface IRes {
    2. code: number;
    3. msg?: string;
    4. data: T;
    5. }
    6. 复制代码

    函数泛形

    我们有一个identity函数,将入参直接返回,那么我们将有如下实现。

    1. function identity(arg) {
    2. return arg;
    3. }
    4. const res = identity(1); // res is any
    5. 复制代码

    我们可以确定的是,我们输入的是1,返回的就是1。但是结果是any,这在我们预期的类型推导层面是不合理的。所以我们需要函数泛形,函数泛形的作用就是为函数提供一个类型的入参,在整个函数体内都可以拿到这个类型入参。用于函数的内变量的声明,函数用于返回类型声明。

    1. function identity(arg): T {
    2. let data: T;
    3. return arg;
    4. }
    5. const res = identity<1>(1); // res 1
    6. 复制代码

    在泛形函数中,我们可以在变量处进行对泛形进行预赋值,将变量将来的值的类型赋值给泛形参数

    1. function identity(arg: T): T {
    2. let data: T;
    3. return arg;
    4. }
    5. const res = identity(1);// res 1 这样不用传入<1>这个类型参数
    6. 复制代码

    对于上面讲到的defineProp其实也是通过泛形参数实现的,实现如下。

    1. function defineProps<TypeProps>(): Readonly<TypeProps>
    2. 复制代码

    于此相似的还有类泛形,作用同样是将泛形参数提供给类,在类里面可以进行使用该参数。

    类型关键字

    为了实现理想的类型约束和维护,我们就会使用到类型关键字,他可以帮助我们进行简单的类型转换,类似于js原生提供的api,比如array会有length属性可以拿到数组长度,Object.keys可以拿到一个对象的key组成的数组。在ts中,也会提供一些小api以完成对类型的裁剪拼接。

    1.[]操作符

    当我们需要从IUser拿到账户信息的部分

    1. interface IUser {
    2. userId: number;
    3. userName: string;
    4. QRCode: string;
    5. age: number;
    6. accountInfo: {
    7. No: number;
    8. passWord: number;
    9. };
    10. }
    11. interface PartOfIUser {
    12. userId: number;
    13. accountInfo: {
    14. No: number;
    15. passWord: number;
    16. };
    17. }
    18. 复制代码

    如此操作,重复度极高,不利于统一类型来源,一改就要改两次。

    bad taste:需要反着写,反直觉

    1. interface PartOfIUser {
    2. userId: number;
    3. accountInfo: {
    4. No: number;
    5. passWord: number;
    6. };
    7. }
    8. interface IUser extends PartOfIUser{
    9. userName: string;
    10. QRCode: string;
    11. age: number;
    12. }
    13. 复制代码

    good taste:

    1. interface IUser {
    2. userId: number;
    3. userName: string;
    4. QRCode: string;
    5. age: number;
    6. accountInfo: {
    7. No: number;
    8. passWord: number;
    9. };
    10. }
    11. interface PartOfIUser {
    12. userId: IUser['userId'];
    13. accountInfo: IUser['accountInfo'];
    14. }
    15. // 或者使用类型工具Pick
    16. type PartOfIUser = Pick<IUser, 'userId' | 'accountInfo'>;
    17. 复制代码

    2.typeof 操作符

    获取某个值的类型属性

    1. const shixin = { name: 'shixin', age: '22' };
    2. type IShixin = typeof shixin;
    3. // 等于
    4. type IShixin = {
    5. name: string;
    6. age: string;
    7. }
    8. let num = 1
    9. type n = typeof num
    10. // 等于
    11. type n = number
    12. 复制代码

    3.keyof 操作符

    TS版本的Object.keys() 可以获取interface的key,组成联合类型

    1. type IShixin = {
    2. name: string;
    3. age: string;
    4. };
    5. type Key = keyof IShixin;
    6. //等于
    7. type Key = 'name' | 'age';
    8. 复制代码

    4.in 操作符

    用于遍历联合类型

    1. type Keys = 'a' | 'b' | 'c';
    2. type Obj = {
    3. [p in Keys]: unknown;
    4. };
    5. // 等于
    6. type Obj = {
    7. a: unknown;
    8. b: unknown;
    9. c: unknown;
    10. }
    11. 复制代码

    5.组合使用操作符

    有如下枚举值和函数,当传入的key为ERROR时,将会把相关信息log出来。

    1. enum LogLevel {
    2. ERROR,
    3. WARN,
    4. INFO,
    5. DEBUG,
    6. }
    7. function printImportant(key: 'ERROR' | 'WARN' | 'INFO' | 'DEBUG', message: string) {
    8. const num = LogLevel[key];
    9. if (num <= LogLevel.WARN) {
    10. console.log('Log level key is:', key);
    11. console.log('Log level value is:', num);
    12. console.log('Log level message is:', message);
    13. }
    14. }
    15. 复制代码

    这个时候我们的printImportant函数的key参数其实是和enum强绑定的。而上面的demo,我们是手动帮key参数写出了所有的枚举值。但是有两个问题

    1. 当LogLevel枚举值添加,我们需要在enum和函数的参数都进行修改。假设有N个函数依赖了此枚举,那么当枚举变动,相关类型就需要手动更改N次。
    2. 当枚举值很多的情况,手动声明就显得太蠢。

    或许我们需要通过LogLevel自动生成一个枚举值的联合类型,而不是手写。这时候我们就可以使用上面说的typeof + keyof了。

    1. enum LogLevel {
    2. ERROR,
    3. WARN,
    4. INFO,
    5. DEBUG,
    6. }
    7. 复制代码
    1. // 第一步,通过typeof 拿到enum的类型属性
    2. enum LogLevel {
    3. ERROR,
    4. WARN,
    5. INFO,
    6. DEBUG,
    7. }
    8. type LogLevelType = typeof LogLevel;
    9. // 等于
    10. type LogLevelType = {
    11. [x: number]: string;
    12. readonly ERROR: LogLevel.ERROR;
    13. readonly WARN: LogLevel.WARN;
    14. readonly INFO: LogLevel.INFO;
    15. readonly DEBUG: LogLevel.DEBUG;
    16. };
    17. // 第二步,通过keyof 拿到上面这个枚举类型属性的key组成联合类型
    18. type LogLevelStrings = keyof LogLevelType;
    19. // 等于
    20. type LogLevelStrings = number | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
    21. 复制代码

    这个LogLevelStrings 就是我们最后需要的啦~整理整理

    1. enum LogLevel {
    2. ERROR,
    3. WARN,
    4. INFO,
    5. DEBUG,
    6. }
    7. /**
    8. * This is equivalent to:
    9. * type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
    10. */
    11. type LogLevelStrings = keyof typeof LogLevel;
    12. function printImportant(key: LogLevelStrings, message: string) {
    13. const num = LogLevel[key];
    14. if (num <= LogLevel.WARN) {
    15. console.log('Log level key is:', key);
    16. console.log('Log level value is:', num);
    17. console.log('Log level message is:', message);
    18. }
    19. }
    20. 复制代码

    类型断言

    当TS没你聪明的时候,类型推断无法推断出正确的类型。需要你手动指定类型,就是类型断言的使用场景。

    还是刚刚的User的例子,我们vue3可能会这样初始化user并且请求数据。

    1. interface IUserRsp {
    2. userId: number;
    3. userName: string;
    4. QRCode: string;
    5. age: number;
    6. accountInfo: {
    7. No: number;
    8. passWord: number;
    9. };
    10. }
    11. // 定义初始user对象
    12. const user = ref({});
    13. const requestUser = async () => {
    14. // 请求user信息
    15. const res: IUserRsp = await request('/api/user/userInfo');
    16. //给user赋值
    17. user.value = res;
    18. // 当我们想用user里面的accountInfo
    19. user.value.accountInfo; // error 类型“{}”上不存在属性“userName”。
    20. };
    21. 复制代码

    这个时候user必然是IUserRsp类型,那么这个user上面必然有accountInfo,但是却报错了。来吧,as!

    这样as

    1. (user as Ref<IUserRsp>).value.accountInfo; // ok
    2. 复制代码

    这样as

    1. (user.value as IUserRsp).accountInfo; // ok
    2. 复制代码

    另一种as可能更加常见:as any。这是anyScript的重要来源。

    1. (user as any).value.accountInfo; // ok
    2. 复制代码

    这样就强制把类型扭转啦,在前两种as,ts的推断系统就会在这一行把user判断成IUserRsp。换句话说,你在这一行代码接管了TS的类型推断。但是其实我们大多数的时候其实并不需要as,as在实践场景更多的是工程师无法(type 无法 = 能力不足|懒得去找报错源头|没时间)维护类型系统而使用的偷懒技巧,在大多数使用as的时候,大家为的并不是解决类型系统问题,而是报错问题。而上面的demo,尽管无数次出现在各种项目的代码中,但其实我们可以通过Partial类型工具来让IUserRsp变成可选IUserRsp。

    1. interface IUserRsp {
    2. userId: number;
    3. userName: string;
    4. QRCode: string;
    5. age: number;
    6. accountInfo: {
    7. No: number;
    8. passWord: number;
    9. };
    10. }
    11. // 定义初始user对象
    12. const user = ref<Partial<IUserRsp>>({}); // 使用Partial
    13. const requestUser = async () => {
    14. // 请求user信息
    15. const res: IUserRsp = await request('/api/user/userInfo');
    16. //给user赋值
    17. user.value = res;
    18. // 当我们想用user里面的accountInfo
    19. user.value.accountInfo; // OK
    20. };
    21. 复制代码

    依然是上面的代码,或许还有一种as

    1. interface IUserRsp {
    2. userId: number;
    3. userName: string;
    4. QRCode: string;
    5. age: number;
    6. accountInfo: {
    7. No: number;
    8. passWord: number;
    9. };
    10. }
    11. // 定义初始user对象
    12. const user = ref({} as IUserRsp);
    13. const requestUser = async () => {
    14. // 请求user信息
    15. const res: IUserRsp = await request('/api/user/userInfo');
    16. //给user赋值
    17. user.value = res;
    18. // 当我们想用user里面的accountInfo
    19. user.value.accountInfo; // ok
    20. };
    21. 复制代码

    这种看起来是安全的,也是当前很多人的首选,因为当我们初始化的时候把user变成了可选的IUserRsp,就意味着在进行如下情况会报错。

    1. user.value.accountInfo
    2. // 可能为
    3. {
    4. No: number;
    5. passWord: number;
    6. }
    7. 或者undefined
    8. //那么如下调用,可能会提示accountInfo没有password,undefined不能直接调用password
    9. user.value.accountInfo.passWord
    10. 复制代码

    当然了,你可以为user制作一个类型守卫,在使用user的时候做一下控制流分析。

    1. const isUser = (user: unknown): user is IUserRsp => !!user['userId'];
    2. 复制代码

    但是在写这些麻烦的代码和在初始化的时候as,选择后者可能更加方便。但后者的风险在于如果有个小笨蛋在还没request的时候就调用了user.value.accountInfo,GG。

    1. // 定义初始user对象
    2. const user = ref({} as IUserRsp);
    3. // 有个小笨蛋在还没request的时候就调用了accountInfo
    4. user.value.accountInfo.passWord; // GG
    5. const requestUser = async () => {
    6. // 请求user信息
    7. const res: IUserRsp = await request('/api/user/userInfo');
    8. //给user赋值
    9. user.value = res;
    10. // 当我们想用user里面的accountInfo
    11. user.value.accountInfo.passWord; // ok
    12. };
    13. 复制代码

    so,ban-as please!

    类型工具

    内置类型工具

    完成了对泛形的理解,掌握了类型关键字了,我们就可以开始使用一些类型工具了。上面讲到了vue为我们提供的类型工具PropType。让我们方便的传入一个参数,就可以生成该参数对应props的类型。在TS内部也提供了很多类型工具。

    属性修饰工具

    Partial 可以将一个interface的所有属性都换成可选的,也就是对接口每一项都添加了可选修饰符 '?'

    1. type Partial = {
    2. [P in keyof T]?: T[P];
    3. };
    4. 复制代码

    Required 可以将一个interface的所有属性都换成必选的,也就对接口每一项都删除了可选修饰符 '?'

    1. type Required = {
    2. [P in keyof T]-?: T[P];
    3. };
    4. 复制代码

    Readonly 可以将一个interface的所有属性都换成仅读的,也就对接口每一项都添加了仅读修饰符 'readonly'

    1. type Readonly = {
    2. readonly [P in keyof T]: T[P];
    3. };
    4. 复制代码

    结构工具

    Record 常用的对象定义方式

    1. type Recordextends string | number | symbol, T> = {
    2. [P in K]: T;
    3. }
    4. 复制代码

    Exclude 联合类型 T中不存在于 U 中的部分

    1. type Exclude = T extends U ? never : T;
    2. type T1 = Exclude<1 | 2, 2>; // 1
    3. 复制代码

    Pick 同lodash的Pick

    1. type Pickextends keyof T> = {
    2. [P in K]: T[P];
    3. };
    4. 复制代码

    **Omit ** 同lodash的Omit

    1. type Omitextends keyof any> = PickExclude>
    2. 复制代码

    怎么实践类型类型工具

    在类型系统中对待类型工具就如同在项目系统中对待lodash,尽管类型体操恶心,但是不需要你完成所有具体实现。

    推荐ts-toolbelt,该库是目前包含工具类型数量最多的。基本能满足你大多数的常规操作。以满足原生的类型工具不够用,自己又不太会写类型工具、或无法花大量时间去维护类型工具。

    一个🌰:

    例如我们有以下的Bar接口,并且想把所有的属性都变成可选的。就像最上面 “object、Object 以及 { }”场景一样,只不过这次的对象嵌套比较深了。

    1. interface Boo{
    2. booA:Array<'3'>
    3. booB:string
    4. }
    5. interface Bar {
    6. a: number;
    7. b: {
    8. b1: boolean;
    9. b2: Array<Boo>;
    10. };
    11. }
    12. 复制代码

    在原生的Partial方法下,得到的结果好像不能满足我们的需求,

    1. interface Boo {
    2. booA: Array<'3'>;
    3. booB: string;
    4. }
    5. interface Bar {
    6. a: number;
    7. b: {
    8. b1: boolean;
    9. b2: Array<Boo>;
    10. };
    11. }
    12. const foobar: Partial<Bar> = {
    13. b: {
    14. b1: true,
    15. b2: [ //类型 "{ booA: "3"[]; }" 中缺少属性 "booB",但类型 "Boo" 中需要该属性。
    16. {
    17. booA: ['3']
    18. }
    19. ]
    20. }
    21. };
    22. 复制代码

    因为原生的Partial只能转化第一层索引为可选的。所以我们需要深层的去转换为可选,也就是我们需要递归的去将每一项都转为可选的,具体的就是,如果该项为一个对象,我们就递归调用这个可选方法。

    1. type Partial = {
    2. [P in keyof T]?: T[P];
    3. };
    4. // to
    5. type DeepPartial = {
    6. [K in keyof T]?: T[K] extends object ? DeepPartial : T[K]
    7. }
    8. // Ok
    9. const foobar: DeepPartial<Bar> = {
    10. b:{
    11. b1:true,
    12. b2:[{
    13. booA:['3']
    14. }]
    15. }
    16. };
    17. 复制代码

    比起如此或许我们可以直接在ts-toolbelt找到 partial 在第二个泛形选择deep,然后我们就得到了一个现成DeepPartial。

    1. import { O } from 'ts-toolbelt';
    2. // OK
    3. const foobar: O.Partial<Bar,'deep'> = {
    4. b: {
    5. b1: true,
    6. b2: [
    7. {
    8. booA: ['3']
    9. }
    10. ]
    11. }
    12. };
    13. 复制代码

    这里我们不再赘述ts-toolbelt的Partial实现逻辑,另一个值得关注的是ts-toolbelt的Partial单测。这里通过TDD的思想完成了目标效果的确立,来保障类型工具的安全。这点是我们值得借鉴的,比较不是每一次需要的类型工具都可以从ts-toolbelt找到答案,总有自己写的时候。

    1. // https://github.com/millsp/ts-toolbelt/blob/master/tests/Object.ts
    2. type PARTIAL_O_FLAT = {
    3. a?: string,
    4. b?: number;
    5. c?: {a: 'a'} & {b: 'b'};
    6. d?: 'string0';
    7. readonly e?: 'string1';
    8. readonly f?: 0;
    9. g?: O;
    10. h?: 1;
    11. j?: 'a';
    12. k?: {a: {b: string}};
    13. x?: () => 1;
    14. };
    15. checks([
    16. checkPartial'flat'>, PARTIAL_O_FLAT, Test.Pass>(),
    17. checkPathPartial'deep'>, ['g', 'g']>, O.Partial'deep'> | undefined, Test.Pass>(),
    18. ])
    19. 复制代码

    如何通过TDD完成一个类型工具

    在TDD的开发模式下,我们会先完成测试用例(目标),再进行对应功能的代码开发(完成目标)。当所有的测试用例通过,就完成了所有的预期功能。当有一个测试用例没有完成,代码就会报错。相当于写了一群监督者检查你的每一个小功能,除了可以帮你检查是否完成预期任务,还可以避免按下葫芦浮起瓢的情况。

    假如我们需要一个完成类型工具ObjPaths,拿到一个interface所有的Path路径,以下注释为预期。

    1. type Cases1 = {
    2. Foo: string;
    3. Bar: number;
    4. };
    5. type caseRes1 = ObjPaths<Cases1> // ObjPaths需要把Case1转成"Foo" | "Bar"
    6. interface Cases2 {
    7. Foo: {
    8. A: string;
    9. B: string;
    10. };
    11. Bar: {
    12. C: number;
    13. };
    14. }
    15. type caseRes2 = ObjPaths<Cases2> // ObjPaths需要把Case2转成"Foo" | "Bar" | "Foo.A" | "Foo.B" | "Bar.C"
    16. type Cases3 = {
    17. Foo: {
    18. A: string;
    19. B: string;
    20. };
    21. Bar: {
    22. C: Array<{
    23. Foo: string;
    24. Bal: number;
    25. }>;
    26. };
    27. };
    28. // ObjPaths需要把Case2转成 "Foo" | "Foo.A" | "Foo.B" | "Bar" | "Bar.C"|"`Bar.C[${number}]`"|`Bar.C[${number}].Bal`|`Bar.C.${number}.Bal`
    29. type caseRes3 = ObjPaths<Cases3>
    30. 复制代码

    基于我们的预期,我们可以根据上面的这些小想法,完成单元测试。js的单测我们通常会使用jest,那么TS我们可以自己写常用的判断方法,这里我们用了type-challenges的一些断言方法。

    1. import type { Equal, Expect, ExpectExtends } from '@type-challenges/utils'
    2. Equal // 如果A和B在类型上是一样的,就回返回true,否则返回false
    3. ExpectExtends// 如果A和B在类型上是A包含B的,就回返回true,否则返回false

    以此得到

    1. type cases = [
    2. Expect<Equal<ObjPaths<Cases1>, "Foo" | "Bar">>,
    3. Expect<Equal<ObjPaths<Cases2>, "Foo" | "Bar" | "Foo.A" | "Foo.B" | "Bar.C">>,
    4. Expect<ExpectExtends<ObjPaths<Cases3>, `Bar.C[${number}]`>>,
    5. Expect<ExpectExtends<ObjPaths<Cases3>, `Bar.C[${number}].Bal`>>,
    6. Expect<ExpectExtends<ObjPaths<Cases3>, `Bar.C.${number}.Bal`>>,
    7. Expect<ExpectExtends<ObjPaths<Cases3>, "Foo" | "Foo.A" | "Foo.B" | "Bar" | "Bar.C">>
    8. ];
    9. 复制代码

    接下来,我们要做的就是解决这个case所有的报错,case解决就算工具完成。TS playground链接

    1. /* -判断当前索引是否为字符串或数字
    2. -如果不是则返回 never
    3. -如果是则根据是否是第一层返回不同的结果的 key
    4. -并且联合上 K 索引的值
    5. -如果是对象则继续递归联合 */
    6. type IsNumber = T extends number ? `[${T}]` : never;
    7. type ObjPaths<
    8. T extends object,
    9. Flag extends boolean = false,
    10. K extends keyof T = keyof T
    11. > = K extends string | number
    12. ?
    13. | (Flag extends true ? `.${K}` | IsNumber : `${K}`)
    14. | (T[K] extends object
    15. ? `${Flag extends true ? `.${K}` | IsNumber : `${K}`}${ObjPaths<
    16. T[K],
    17. true
    18. >}`
    19. : never)
    20. : never;
    21. 复制代码

    如何找到类型工具

    对于类型编程,国内外的环境可能还是有比较大的差距。所以当你在使用百度去查找你想要的类型工具,基本是不如自己撸一个来的快。但是Google相对来说会很容易找到你需要的工具答案。如果内置函数,工具库都无法满足你的诉求时,建议去Google进行搜索引擎努力。另一个是 type-challenges 类型版本的leetCode,内置了非常多的类型问题,你也可以从这上面找到对应的答案或实现灵感。

    总结

    1. 本篇文档讲述了一些基本的类型操作,和一些小的知识点,讲了一些实际场景的类型问题以及如何完成一个类型工具,并介绍了一些常用的类型工具轮子。类型收敛、类型工具非常重要,优化类型收敛、类型工具都是TS官方近两年迭代的主题,比如关键词satisfies,Awaited类型工具、模版字符串、优化控制流分析等等。
    2. 一篇文章无法覆盖所有的类型API,对于类型的维护,并不需要知道所有的API,但需要清晰将类型收敛到什么程度,才能够满足你的需求。ban了as,ban了any,学会基本的类型操作,知道2022年的类型系统可以做到什么,能够找到你想要的类型工具,积极的使用内置和封装的类型工具合理的去维护项目的类型系统。或许用类型系统写一个国际象棋回报相对较低,但是用用Omit,Google找个ObjPaths难度并不大。

    给大家推荐一个实用面试题库

    1、前端面试题库 (面试必备)            推荐:★★★★★

     地址:web前端面试题库

  • 相关阅读:
    多重循环控制练习
    高德百度的逆地理编码接口对比
    报表技术POI导入导出复杂样式Excel和百万数据(Java代码、模板、数据)
    Linux网络编程-详解http协议
    python中hasattr()函数用法详解
    STC51单片机学习笔记10——AD测试(stc15w408as)
    数据结构初阶--栈和队列(讲解+类模板实现)
    深入浅出Java多线程(五):线程间通信
    同轴线的仿真
    部署LVS—NAT模式集群
  • 原文地址:https://blog.csdn.net/weixin_42981560/article/details/127890343