关于 TypeScript 一些高级用法的总结

前端达人

共 8819字,需浏览 18分钟

 ·

2021-09-03 12:51

前言

TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。笔者使用typescript也差不多快一年了,使用Ts也感受颇深,Ts无疑增强我们代码的规范性和可维护性,但是也同时增加了我们的开发负担,但是其实对一个项目的长期而言,我认为这是值得的,这篇文章我们来讲一下Ts的高级用法,适合刚接触Ts的同学但是学的没有那么深的同学,至于我们的vue源码分析,我先暂更一周,在以后的时间将会继续讲到。

一、类型

1. unknow

unknown 指的是不可预先定义的类型,在很多场景下,它可以替代 any 的功能同时保留静态检查的能力。

const num: number = 10;(num as string).split('') // error 类型 "number" 到类型 "string" 的转换可能是错误的,因为两种类型不能充分重叠。如果这是有意的,请先将表达式转换为 "unknown"。(num as unknown as string).split('')

这个时候 unknown 的作用就跟 any 高度类似了,你可以把它转化成任何类型,不同的地方是,在静态编译的时候,unknown 不能调用任何方法,而 any 可以。

const foo: unknown = 'string';foo.substr(1);     // Error: 静态检查不通过报错const bar: any = 10;bar.substr(1);    // Pass: any类型相当于放弃了静态检查

unknown 的一个使用场景是,避免使用 any 作为函数的参数类型而导致的静态类型检查 bug:

function test(input: unknown): number {  if (Array.isArray(input)) {    return input.length;    // Pass: 这个代码块中,类型守卫已经将input识别为array类型  }  return input.length;      // Error: 这里的input还是unknown类型,静态检查报错。如果入参是any,则会放弃检查直接成功,带来报错风险}
2. enum

枚举的话可以增强我们代码的可读性,例如我在业务中需要个后端传一个回答问题的类型type,回复文本的话需要传一个1,回复图片的会传2,如果直接在调用接口的时候传一个2那肯定会让别人看上去很疑惑,如此一来我们可以定义一个枚举,需要传值例如传一个视频,我们可以传AnswerMessageType.video,而且在写接口规范参数类型的时候我们可以规定这个type为AnswerMessageType,如此一来你要是传个7就肯定不能通过的。

/** * 回复问题的消息类型 */ export enum AnswerMessageType {    text = 0,    pictrue = 1,    video = 2,    voice = 3,    file = 4,    /**     * Srceent Rock 链接     */    srlink = 6  }  
3. void

在 TS 中,void 和 undefined 功能高度类似,可以在逻辑上避免不小心使用了空指针导致的错误。void 和 undefined 类型最大的区别是,你可以理解为 undefined 是 void 的一个子集,当你对函数返回值并不在意时,使用 void 而不是 undefined。举一个 React 中的实际的例子。

// Parent.tsxconst  Parent: FC = () => {    const getValue = (): number => { return 2 };     /* 这里函数返回的是number类型 */    // const getValue = (): string => { return 'str' };  /* 这里函数返回的string类型,同样可以传给子属性 */    return <Child getValue={getValue} />}// Child.tsxtype Props = {  getValue: () => void;  // 这里的void表示逻辑上不关注具体的返回值类型,number、string、undefined等都可以}function Child({ getValue }: Props) => <div>{getValue()}</div>

运算符

1. 非空断言运算符 !

这个运算符可以用在变量名或者函数名之后,用来强调对应的元素是非 null|undefined 的

function onClick(callback?: () => void) {    callback();    // 参数是可选入参,加了这个感叹号!之后,TS编译不报错}

这个符号的场景,特别适用于我们已经明确知道不会返回空值的场景,从而减少冗余的代码判断,如 React 的 Ref。以下是笔者工作中写的一个自定义hook,左右两个div,左边放图片,右边是一段文字文字的多少是不确定的,左边的图片根据右边的盒子来计算大小,如下使用了多个ref,但是我们在一些情况写可以!来判断e.target!或者divRef.current!来进行一个书写

export function useCalcImg() {  const [maxWidth, setMaxWidth] = useState(0)  const [maxHeight, setMaxHeight] = useState(0)  const [imgWidth, setImgWidth] = useState(0)  const [imgHeiht, setImgHeight] = useState(0)  const imgRef = useRef(null)  const divRef = useRef<HTMLDivElement>()  const handleImgLoad = useCallback((e) => {    let img = e.target as HTMLImageElement    setImgWidth(img.naturalWidth)    setImgHeight(img.naturalHeight)  }, [])  const calc = () => {    if (maxWidth && imgWidth && imgRef.current) {      let div = divRef.current      setTimeout(() => {        let { width, height } = Util.calc.fixedImgScale(imgWidth, imgHeiht, div.clientWidth, div.clientHeight)        imgRef.current.width = width        imgRef.current.height = height        removeClass(imgRef.current, 'hidden')      }, 100)      imgRef.current.width = 100    }  }
const imgRefCallback = useCallback((img) => { imgRef.current = img }, []) useEffect(() => { calc() }, [imgRef, divRef, maxWidth, imgHeiht]) const refCallback = useCallback((e) => { divRef.current = e if (e) { const { clientWidth, clientHeight } = e setMaxWidth(clientWidth) setMaxHeight(clientHeight) } }, []) return { imgRefCallback, handleImgLoad, refCallback }}
2. 可选链操作运算符 ?

相比上面!作用于编译阶段的非空判断,?.这个是开发者最需要的运行时(当然编译时也有效)的非空判断。

// 例如后端有个参数标识flag,有标识的时候传flag: 1, 没有标识的时候不传data?.flag

?.用来判断左侧的表达式是否是 null | undefined,如果是则会停止表达式运行,可以减少我们大量的&&运算。

比如我们写出a?.b时,编译器会自动生成如下代码

let b = a !== null && a !== void 0 ? a : 10;

三、操作符

1. keyof

keyof 可以获取一个类型所有键值,返回一个联合类型,如下:

type Person = {  name: string;  age: number;}type PersonKey = keyof Person;  // PersonKey得到的类型为 'name' | 'age'
2. typeof

typeof 是获取一个对象/实例的类型,如下

interface Person {    name: string,    age: number}const people: Person = {    name: 'thl',    age: 22}const P = typeof people // { name: string, age: number | undefined }const me: typeof people = { name: 'wsy', age: 21 }
3. in

in 只能用在类型的定义中,可以对枚举类型进行遍历,如下:

// 这个类型可以将任何类型的键值转化成number类型type TypeToNumber<T> = {  [key in keyof T]: number}

keyof返回泛型 T 的所有键枚举类型,key是自定义的任何变量名,中间用in链接,外围用[]包裹起来(这个是固定搭配),冒号右侧number将所有的key定义为number类型。

const person: TypeToNumber<Person> = {name: 21, age: 21}// [ 自定义变量名 in 枚举类型 ]: 类型

四、泛型

泛型在 TS 中可以说是一个非常重要的属性,它承载了从静态定义到动态调用的桥梁,同时也是 TS 对自己类型定义的元编程。泛型可以说是 TS 类型工具的精髓所在,也是整个 TS 最难学习的部分。基本使用

// 普通类型定义type Person<T> = { name: string, type: T }// 普通类型使用const person: Person<number> = { name: 'ww', type: 20 }
// 类定义class People<T> { private type: T; constructor(type: T) { this.type = type; }}// 类使用const people: People<number> = new People<number>(20); // 或简写 const people = new People(20)
// 函数定义function swipe<T, U>(value: [T, U]): [U, T] { return [value[1], value[0]];}

例如深度克隆

export function deepCopy<T extends Record<any, any>>(data: T): T {  const t = typeOf(data)  let o
if (t === 'array') { o = [] } else if (t === 'object') { o = {} } else { return data }
if (t === 'array') { for (let i = 0; i < ((data as any) as any[]).length; i++) { o.push(deepCopy(data[i])) } } else if (t === 'object') { for (let i in data) { o[i] = deepCopy(data[i]) } } return o}

五、高级泛型

1. Partial

此工具的作用就是将泛型中全部属性变为可选的。

type Partial<T> = {    [P in keyof T]?: T[P]}

举个栗子,此时happy这个方法可以不传

type Person = {    name: string    age: number    happy: () => void}const me: Partial<Person> = {    name: 'thl',    age: 22}
2、Record<K, T>

此工具的作用是将 K 中所有属性值转化为 T 类型,我们常用它来申明一个普通 object 对象。

type Record<K extends keyof any,T> = {  [key in K]: T}

举个栗子

const me: Record<string, string> = { 'name': 'thl', 'tag': '打工人' }
3. Pick<T, K>

此工具的作用是将 T 类型中的 K 键列表提取出来,生成新的子键值对类型。

type Pick<T, K extends keyof T> = {  [P in K]: T[P]}

举个栗子

interface Person {    name: string    age: number    do: () => void}type PeopleInfo =  Pick<Person, 'name' | 'age'>// 将name和age两个类型提取出来,不用从新定义类型const me: Person { name: 'thl', age: 22}
4: Exclude<T, U>

此工具是在 T 类型中,去除 T 类型和 U 类型的交集,返回剩余的部分。

type Exclude<T, U> = T extends U ? never : T

举个栗子

type PersonOne = {    name: string    age: number    sleep: () => void}type PersonTwo = {    name: string    age: number    eat: () => void}
type Person = Exclude<PersonOne, PersonTwo> 等价于type PersonDo = { sleep: () => void eat: () => void }
5. Omit<T, K>

此工具可认为是适用于键值对对象的 Exclude,它会去除类型 T 中包含 K 的键值对。

type Omit = Pick<T, Exclude<keyof T, K>>

举个栗子

type Person = Omit<PersonOne, 'age'>const me: Person = {    name: 'thl',    sleep: () => console.log('i will')}
6. ReturnType

此工具就是获取 T 类型(函数)对应的返回值类型:

type ReturnType<T extends (...args: any) => any>  = T extends (...args: any) => infer R ? R : any;

看源码其实有点多,其实可以稍微简化成下面的样子:

type ReturnType<T extends func> = T extends () => infer R ? R: any;

举个栗子

function foo(x: string | number): string | number {// Todo}type FooType = ReturnType<foo>;  // string | number
7. Parameters

此工具就是获取T类型(函数)对应的参数类型

type Parameters<T> = T extends (...args:string[]) => any ? string[] : any;

老规矩举个栗子

type Fn = (a: string, b: number) => voidtype FnParams = Parameters<Fn>

其实还有一些高级类型这里就不一一列举了,有兴趣的可以去逛下官网www.typescriptlang.org

总结

Ts的使用有很多技巧,我们在日常使用中可能没有那么容易遇到,在我们阅读一些源码的时候就会了解到很多,还有一个需要总结的问题是关于type和interface的区别和我们如何选择的问题,本质上没有什么区别, 从扩展的角度上来看,type比interface好些,使用&可以少些一些代码

type Person = {    name: string    age: number}// 扩展type NewPerson = Person & {do: () => void}interface People {    happyStatus: true    eat: () => void}// 扩展interface NewPeople extends People {    name: string}

另外 type 有一些 interface 做不到的事情,比如使用 | 进行枚举类型的组合,使用typeof获取定义的类型等等。

不过 interface 有一个比较强大的地方就是可以重复定义添加属性,比如我们需要给window对象添加一个自定义的属性或者方法,那么我们直接基于其 Interface 新增属性就可以了。

declare global {    interface Window { MyNamespace: any; }}

个人习惯一般我使用type多一些

栗鼠怪

转自:https://juejin.cn/post/6983576636025208846

浏览 56
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报