基础知识概述
伴随着ts4.3的发布,ts4.4也已经处于beta阶段了,ts成为了一个前端规范的趋势,随之vue、react和其他的plugin都开始了使用ts进行编写,ts已经成为了一个前端开发者所必需熟悉的一个东西。能否写出准确的ts类型也成为了一段代码的质量的评判条件之一。
ts类型并不是只能有类型定义,然后给逻辑代码用这么简单。也并不是说代码懂得了一个any这样的全能类型,然后就到处使用any,只关心逻辑代码这块(能跑就行!!!!)。
1、是开发和维护过程中的工具
对于ts类型,他也是有自己的编程逻辑的。对于它,我们可以将它理解成一个我们项目开发与维护的工具,这个取决于我们对这个工具的了解程度和利用的程度,利用的好,它是规范我们的逻辑代码的一大利器,代码中的变量便会在明确的类型指引下快速且高效的开发。
- 约束某一块代码的具体功能
比如说在一个函数中,我们因为有了一个函数的约束,我们可以很明确的知道这个段逻辑块要完成的是一个什么样的功能,不得不说的是,很多人觉得ts是个累赘,那就是很多人都是先写逻辑,后写类型的,就是一种差不多的想法,代码能跑就行,最后只能写成了anyscript。 比如说:
1 | const foo = (arg: A): B => { |
我们便能知道这个函数是一个要将A类型的变量处理处理成B类型的函数,这段代码有这样的作用就通过类型就可以显而易见了,当然前提是A和B的类型是明确的,不能写一个any,Object草草了事,当然在返回类型是也要尽量的准确点,比如说,有的时候为了一个求一个逻辑代码写的顺畅,盲目的让类型迎合变量。
1 | const foo = (a: A): string | number | boolean => { |
1 | const foo = (a: string | number | object | boolean): B => { |
这样的做法在ts编程的时候是不可取(不建议)的,其实在纯js中也是不建议这样的,这样失去了逻辑快单一功能的原则,会让后续的对它的维护显得特别的困难,在之后的调试过程中也会显得格外的困难。
可能,我们会真的碰到这个函数逻辑就是要这样的耦合,那么我们不妨试试另一种类型声明的方式。
1 | interface foo {(a: A, b: string): void} // 参数a为类型A时,那么b的类型为string |
这三个都是给一个函数声明类型,然后可以很有效的做到逻辑耦合但是类型不耦合,同时也对函数的调用起到了类型校验的作用,不再是像以前写着一个联合类型,让人调用函数的时候觉得这个类型有点傻乎乎的样子,什么也不懂。
这样的方式可以在很大的程度上,让函数的调用变得轻松,不至于让写出的东西让别人无法调用或者类型靠as去断言。
1 | declare interface Foo { |
注:这样声明的类型只能用interface,原因可以见下文的interface、type和class的区别
对于函数的逻辑块的类型声明可以这样,同理,组件中我们也可以做到。 其实组件中特别是想要复用性高一点的组件,一般都不会太建议太高的耦合度了,但是,方法也是有的了。
在react中,如果是函数式组件,我们可以跟上面说的函数定义一样的去做
1 | interface IProps { |
同样,这样的耦合度对于这个组件的编写还是不太好的,但是很大程度上可以简化组件的调用,让别人可以更好的去调用它。 在class组件中,这样的做法会显得更加简便
1 | interface ComponentA { |
这样子对组件的编写过程起到的作用还是比较小的,但是对于组件的调用却有很大的意义,它可以查出组件错误的调用方式。
- 约束静态数据
作为工具,说白了就是没有它也一样,照样可以做出我想要的东西。但是ts能够一直发展过来,能够受到这么多的前端开发者的青睐自有它的原因。它确实是可以很好的去约束我们的代码,约束我们开发过程中所制造出的各种数据,这个也是取决于,自己对ts的理解程度的。 比如说我们在定义如下数据的类型时
1 | const schedule = { |
在只知道interface,type的时候会一项一项的列出来
1 | type ISchedule = { |
然后再深入,知道了[in],然后又会觉得,类型不过如此,会直接在写出
1 | type ISchedule = { |
之后,更加深入的去知道了自带的工具类型
1 | type ISchedule = { |
很明显,对ts的使用程度就可以体现出来了,写法的不同,ts发挥的作用也都是不同的。
后面两种的写法很明显只是为了规避eslint的报错而写的类型的,定义太宽泛,这个时候如果是前端自己写类型定义可以使用类型的模板字符串
1 | type N = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |
模板字符串,它集成了[in]语法糖,让所有的可能类型自由组合,可以自动对所有的情况做一次遍历。 当然这样写只是为了提示一下ts的新特性中还有类型模板字符串,但是也可以看得出,ts的类型也越来越完善,可以让类型定义的越来越精确。有了这个模板字符串,很多的以前我们就写一个string的类型,我们都可以给定一个更加精确的定义,来保证我们的类型的准确性 比如说,我们给request定义url的时候我们就可以不写string,完全可以定义一个独有的IUrl类型,来规范url的编写。
1 | type IUrl = `/${string}` |
这样就可以避免犯前面忘了加 / 这样的低级错误。 此外,类型模版字符串甚至可以用于校验电话号码,时间格式等字符串类型的数据,预防静态数据认为输入是校验太少而出现的错误。
- 类型的断言
其实ts作为工具,它和eslint的功能差不多,都是一个规范代码书写,可快发提效的手段。同时也会遇到一些ts无法准确做出判断的时候,毕竟ts是不参与逻辑代码的计算的,应该说,是不支持解耦之后的代码的类型运算。
1 | type Foo1 = { |
在这种前后耦合的情况下类型还是可以会能有自己的推导的,具体的,可以去看类型的合成与拆分,这也是在某个函数一定要耦合的时候建议的做法。
好了以上的还是题外话,想提醒一下,是不是类型的联合都还搞不清楚。对于一些情况,比如说我们在一些dialog中,我们有时候会习惯用一个对象来驱动弹窗的显隐。让弹窗的显隐通过是否有数据驱动
1 | <Dialog visible={!!data} close={data.close}> // error, data可能为undefined |
这种情况下就难免会给data定义undefined的联合类型的了,到了一些方法中,这个主要是因为数据与视图解耦了,ts就会提醒你某个参数有可能为undefined,但是为undefined的时候数据都是不执行的,其实前面做一个非空检验也没什么大不了的,但是这个时候就看的出ts就比较傻了,此时我们的断言就可以用上了
1 | <Dialog visible={!!data} close={data!.close}> // ok, 这里可以用一个非空断言 |
大部分的时候非空断言( ! ),我们基本上就够用了,可以解决大部分类型推导不过来的问题。
但是还有很多时候,ts的使用程度不同的人会出现不一样的情况,有的人对类型的定义严格,有的人定义的宽松,为什么会这样可以看前面的概述。但是问题还是要解决的,我们这个时候如果真的非常肯定不会出问题,那么我们不妨试试as断言。
1 | type Foo1 = { |
从这个小例子中我们可以看出,断言对很多类型逃避主义的人其实也是一个非常大的福音,类型定义的时候宽泛就好了,衔接不上的时候直接as unknown as …、as any as …。如果有这种的行为,我也只能说,干的漂亮!ts技术又有了一点提升,只要代码能跑,还真让别人挑不出一点ts的毛病。 话说回来,还是好好的重视类型报错、严格的定义类型吧,如果真的是有非要有耦合的情况,可以看看上面本节的第一点说的方法吧。ts要不了多少时间的,真的可以避免错误,还有减少很多找bug的时间。
- 全局类型声明
应该很多人想过一个问题,为什么我们可以不需要引入,就可以用Record、Omit、Partail这样的工具类型。还有就是,我们自己开发过程中其实也写出了很多的好用的类型,有的是可以完全脱离某个项目,直接到处都可以用的,比如说我们Omit的源码。
1 | type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; |
它的第二个参数受的是any约束,虽然也有它的原因,但是我们完全可以自己再写一个MyOmit
1 | type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>; |
这样就可以使得MyOmit更符合我们的需求
下面的问题来了,如果我们只是这样的定义一下,别的地方想使用每次都得要去import,但是这个东西本来就是开发的时候稍微用一样,打包的时候都是去掉的。那么有什么办法可以别的地方直接用就好了??
其实这个也很简单,只需要在根目录下定义一个.d.ts,文件就好了
1 | // public.d.ts |
这样就好了,就可以在整个项目中使用了。
如果你以为这一点就这么完了,那你想的肯定是太简单了,另外拓展一下,其实类型也是可以改写的😂😂。
比如说,在使用Object.entries的时候,通过lib.es2017.d.ts中的源码可以看到,entries的类型定义还是有点不太好,不太适合日常的ts类型开发。
1 | /** |
这个时候,我们就可以考虑一下改写它的类型,当然不是去修改编辑器的代码,别想太多。
1 | // .d.ts |
2、是一个具有编程思想的语言
上一节中我们从typescript是一个规范开发的工具入手,讲述了ts可以有的几大功能。那么这一节,就讲述一下typescript其实也是有一定的编程思想的。它也是有自己的变量声明、条件语句、循环语句、作用域的,当你习惯了这个编程思想之后,就能够更加深刻的感受到ts所带来的便利
- interface、type、class的区别
很多人对interface、type的理解仅仅只是一个声明类型方式的区别。 但是事实上,它就是类型定义的一个区别。 只不过,有以下几点的区别
1、type,class定义的类型不可以重复,interface定义的类型可以重复
1 | type Record = {...} // error, Record已经被定义过 |
这就是在上节中所讲的类型可以被重写的另一个知识点。 2、type声明的对象可以使用[in],interface,class的不可以
1 | type Keys = "小王" | "小文" |
3、interface,class使用extends、implements组合类型,type则可以通过&, |符号组合、合并类型
1 | interface Animals1 { |
4、interface定义的类型可以被改写,type、class定义的类型不可以被改写
1 | // a.d.ts |
1 | // b.d.ts |
5、type可以使用typeof、keyof、infer去反推类型,interface、class不可以
1 | const foo = { |
从以上的几点区别可以看出,类型的定义上,interface和class更像是给ecmascript增加了接口的概念,让类型与代码可以耦合,真正的将js变成了强类型语言(当然总是联合类型就另说了,基本上不会在意这么多了,基本上就不会管类型声明上的区别了)。
1 | interface Animal1 { |
可以看出来使用的还是java里面的那一套。 之后的type的类型定义,便又是另一个编程思想了,它就是一个类型编程的思想了。它可以使用typeof,keyof,infer等各种类型推导。同时可以使用[in]等对类型各种各样的推导操作
1 | const defaultData = { |
同时也可以开始有了通过各种工具类型(有自带的也有自己写的 ),对类型进行各种运算
1 | const defaultData = { |
1 | type Foo<T> = T extends { a: infer U; b: infer U } ? U : never; |
- 类型的推导
类型的推导那就是比较考验对类型够不够理解了,主要涉及到的ts知识点就为typeof、keyof、infer。 不过值得说的就是infer对于业务代码中的类型使用还是没那么多的。用的最多的还是typeof和keyof,它可以让人更快速的去得到想要的类型
1 | type Foo1 = { |
而对于infer,我们很多时候可以用它来制作工具类型,我们这个时候是可以参考Parammeters、ReturnType等工具类型
1 | type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never; |
- 类型的条件语句
这个又是一个关于extends的作用了,extends它不仅可以在interface上可以使用继承(或许有的人的理解仅限于此)。同时可以有一个约束的功能,其实很像继承的反推
1 | interface Lengthwise { |
1 | type Foo<T extends 0 | 1 | 2> = T extends 0 | 1 ? `${T}${N}` : `${T}${0|1|2|3}` |
- 类型的循环遍历
既然是可以编程的,那就不可以缺少循环遍历的语句了。ts的类型也是可以通过[in]对类型进行循环遍历的。同样的,这只能在type中使用
1 | type Foo = { |
甚至乎,我们还可以使用类型模版字符串,快速的写出更加标准的类型
1 | type Foo = { |
同样,作为
- 静态数据的类型
说到这个,我们就不得不说的了,很多时候,我们写一个string,number其实也算是一个类型的敷衍 比如说,我们在定义一个mode,或者status时
1 | type Foo = { |
这样的类型定义,其实很明显也是在逃避的,毕竟后面的status或者mode也有可能是要用的。后面的代码很有可能就会这样
1 | if(status === 0) { |
这样就是让后面的维护者风中凌乱了,很不利于维护,同时那个number和string也很容易写错。 其实对于这种静态的类型,建议是不应该用个number和string的。 对于前端自产自销的mode或者status,不妨试试
1 | if(status === 0) { |
如果是对于后端返回的而且要用到的,这个时候就可以使用枚举了
1 | enum IStatus { |
通过这个就不得不说一下了,其实enum与相似,它既可以参与逻辑,也可以参与类型。在类型中它可以表示一个基本类型。在逻辑中,它就可以充当出一个constant的作用了 对于静态的数据,可以做一点拓展知识,那就是as const
1 | cosnt arr = ['a', 'b', 'c'] |
1 | cosnt arr = ['a', 'b', 'c'] as const |
- 动态类型的定义
是编程,那么就得有变量的声明,和数据的自顶向下的数据流,ts中也是一样的,这个东西便是泛型了。有了它,就可以正式的将类型带入了编程的行列。 逻辑代码上泛型可以跟函数跟类进行耦合,去写出一个更加可用的模块
1 | const foo = <T extends {type: 'a'|'b', val: any}>(arg: T) => { |
1 | class Foo<T, U> { |
类型编程上我们可以通过泛型,写出各种好用的工具类型,提升我们的开发效率 我们可以拿我们熟知的protable的类型定义说起
1 | export declare type ProSchema<T = Record<string, unknown>, Extra = unknown, V = ProSchemaComponentTypes, ValueType = 'text'> = { |