关于typescript:TypeScript高级用法

56次阅读

共计 9762 个字符,预计需要花费 25 分钟才能阅读完成。

hi,豆皮粉儿们,明天又和大家见面了,本期分享的是由 bytedancer“米兰的小铁匠”,带来的 TypeScript 高级应用,
实用于对 TypeScript 曾经有所理解或者曾经理论用过一段时间的同学,
本文别离从类型、运算符、操作符、泛型的角度来零碎介绍常见的 TypeScript 文章没有好好解说的性能点,
最初再分享一下作者的实际经验。

作者:米兰的小铁匠

一、类型

unknown

unknown 指的是 不可事后定义的类型,在很多场景下,它能够代替 any 的性能同时保留动态查看的能力。

const num: number = 10;
(num as unknown as string).split('');          // 留神,这里和 any 一样齐全能够通过动态查看

这个时候 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,则会放弃查看间接胜利,带来报错危险
}

void

在 TS 中,void 和 undefined 性能高度相似,能够在逻辑上防止不小心应用了空指针导致的谬误

function foo() {}          // 这个空函数没有返回任何值,返回类型缺省为 void
const a = foo();        // 此时 a 的类型定义为 void,你也不能调用 a 的任何属性办法

void 和 undefined 类型最大的区别是,你能够了解为 undefined 是 void 的一个子集,当你对函数返回值并不在意时,应用 void 而不是 undefined。举一个 React 中的理论的例子。

// Parent.tsx
function Parent(): JSX.Element {const getValue = (): number => {return 2};           /* 这里函数返回的是 number 类型 */
  // const getValue = (): string => { return 'str'};        /* 这里函数返回的 string 类型,同样能够传给子属性 */
  return <Child getValue={getValue} />
}
// Child.tsx
type Props = {getValue: () => void;  // 这里的 void 示意逻辑上不关注具体的返回值类型,number、string、undefined 等都能够
}
const Child = ({getValue}: Props) => <div onClick={() => getValue()}>click</div>;

never

never 是指没法失常完结返回的类型,一个必定会报错或者死循环的函数会返回这样的类型。

function foo(): never { throw new Error('error message') }  // throw error 返回值是 never
function foo(): never { while(true){}}  // 这个死循环的也会无奈失常退出
function foo(): never { let count = 1; while(count){count ++;} }  // Error: 这个无奈将返回值定义为 never,因为无奈在动态编译阶段间接辨认出

还有就是永远没有相交的类型

type human = 'boy' & 'girl' // 这两个独自的字符串类型并不可能相交,故 human 为 never 类型

不过任何类型联结上 never 类型,还是原来的类型

type language = 'ts' | never   // language 的类型还是 'ts' 类型

对于 never 有如下个性:

  • 在一个函数中调用了返回 never 的函数后,之后的代码都会变成deadcode
function test() {foo();                  // 这里的 foo 指下面返回 never 的函数
  console.log(111);         // Error: 编译器报错,此行代码永远不会执行到
}
  • 无奈把其余类型赋给 never
let n: never;
const o: any = {};
n = o;  // Error: 不能把一个非 never 类型赋值给 never 类型,包含 any

对于 never 的这个个性有一些很 hack 的用法和探讨,比方这个知乎下的 尤雨溪的答复

二、运算符

非空断言运算符 !

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

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

查看编译后的 ES5 代码,竟然没有做任何防空判断

function onClick(callback) {callback();
}

这个符号的场景,特地实用于咱们曾经明确晓得不会返回空值的场景,从而缩小冗余的代码判断,如 React 的 Ref

function Demo(): JSX.Elememt {const divRef = useRef<HTMLDivElement>();
  useEffect(() => {divRef.current!.scrollIntoView();         // 当组件 Mount 后才会触发 useEffect,故 current 肯定是有值的
  }, []);
  return <div ref={divRef}>Demo</div>
}

可选链运算符 ?.

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

obj?.prop    obj?.[index]    func?.(args)

?. 用来判断左侧的表达式是否是 null | undefined,如果是则会进行表达式运行,能够缩小咱们大量的 && 运算

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

a === null || a === void 0 ? void 0 : a.b;

这里波及到一个小知识点: undefined这个值在非严格模式下会被从新赋值,应用 void 0 必然返回真正的 undefined

空值合并运算符 ??

?? 与 || 的性能是类似的,区别在于?? 在左侧表达式后果为 null 或者 undefined 时,才会返回右侧表达式

比方咱们书写了const b = a ?? 10,生成的代码如下

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

而 || 表达式,大家晓得的,则对 false、”、NaN、0 等逻辑空值也会失效,不适于咱们做对参数的合并

数字分隔符_

const num:number = 1_2_345.6_78_9

_能够用来对长数字做任意的分隔,次要设计是为了便于数字的浏览,编译进去的代码是没有下划线的,请释怀食用

三、操作符

键值获取 keyof

keyof 能够获取一个类型所有键值,返回一个联结类型,如下

type Person = {
  name: string;
  age: number;
}
type PersonKey = keyof Person;  // PersonKey 失去的类型为 'name' | 'age' 

keyof 的一个典型用处是限度拜访对象的 key 合法化,因为 any 做索引是不被承受的

function getValue (p: Person, k: keyof Person) {return p[k];  // 如果 k 不如此定义,则无奈以 p[k]的代码格局通过编译
}

总结起来 keyof 的语法格局如下

类型 = keyof 类型

实例类型获取 typeof

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

const me: Person = {name: 'gzx', age: 16};
type P = typeof me;  // {name: string, age: number | undefined}
const you: typeof me = {name: 'mabaoguo', age: 69}  // 能够通过编译

typeof 只能用在具体的对象上,这与 js 中的 typeof 是统一的,并且它会依据左侧值主动决定应该执行哪种行为

const typestr = typeof me;   // typestr 的值为 "object"

typeof 能够和 keyof 一起应用(因为 typeof 是返回一个类型嘛),如下

type PersonKey = keyof typeof me;   // 'name' | 'age'

总结起来 typeof 的语法格局如下

类型 = typeof 实例对象

遍历属性 in

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

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

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

于是能够这样应用了

const obj: TypeToNumber<Person> = {name: 10, age: 10}

总结起来 in 的语法格局如下

[自定义变量名 in 枚举类型]: 类型

四、泛型

泛型在 TS 中能够说是一个十分重要的属性,它承载了从动态定义到动静调用的桥梁,同时也是 TS 对本人类型定义的元编程。泛型能够说是 TS 类型工具的精华所在,也是整个 TS 最难学习的局部,这里专门分两章总结一下。

根本应用

泛型能够用在一般类型定义,类定义、函数定义上,如下

// 一般类型定义
type Dog<T> = {name: string, type: T}
// 一般类型应用
const dog: Dog<number> = {name: 'ww', type: 20}

// 类定义
class Cat<T> {
  private type: T;
  constructor(type: T) {this.type = type;}
}
// 类应用
const cat: Cat<number> = new Cat<number>(20); // 或简写 const cat = new Cat(20)

// 函数定义
function swipe<T, U>(value: [T, U]): [U, T] {return [value[1], value[0]];
}
// 函数应用
swipe<Cat<number>, Dog<number>>([cat, dog])  // 或简写 swipe([cat, dog])

留神,如果对一个类型名定义了泛型,那么应用此类型名的时候肯定要把泛型类型也写上去。

而对于变量来说,它的类型能够在调用时推断进去的话,就能够省略泛型书写。

泛型的语法格局简略总结如下

类型名 < 泛型列表 > 具体类型定义

泛型推导与默认值

下面提到了,咱们能够简化对泛型类型定义的书写,因为 TS 会主动依据变量定义时的类型推导出变量类型,这个别是产生在函数调用的场合的

type Dog<T> = {name: string, type: T}

function adopt<T>(dog: Dog<T>) {return dog};

const dog = {name: 'ww', type: 'hsq'};  // 这里依照 Dog 类型的定义一个 type 为 string 的对象
adopt(dog);  // Pass: 函数会依据入参类型推断出 type 为 string

若不实用函数泛型推导,咱们若须要定义变量类型则必须指定泛型类型

const dog: Dog<string> = {name: 'ww', type: 'hsq'}  // 不可省略 <string> 这部分

如果咱们想不指定,能够应用泛型默认值的计划

type Dog<T = any> = {name: string, type: T}
const dog: Dog = {name: 'ww', type: 'hsq'}
dog.type = 123;    // 不过这样 type 类型就是 any 了,无奈主动推导进去,失去了泛型的意义

泛型默认值的语法格局简略总结如下

泛型名 = 默认类型

泛型束缚

有的时候,咱们能够不必关注泛型具体的类型,如

function fill<T>(length: number, value: T): T[] {return new Array(length).fill(value);
}

这个函数承受一个长度参数和默认值,后果就是生成应用默认值填充好对应个数的数组。咱们不必对传入的参数做判断,间接填充就行了,然而有时候,咱们须要限定类型,这时候应用 extends 关键字即可

function sum<T extends number>(value: T[]): number {
  let count = 0;
  value.forEach(v => {count += v});
  return count;
}

这样你就能够以 sum([1,2,3]) 这种形式调用求和函数,而像 sum(['1', '2']) 这种是无奈通过编译的

泛型束缚也能够用在多个泛型参数的状况

function pick<T, U extends keyof T>(){};

这里的意思是限度了 U 肯定是 T 的 key 类型中的子集,这种用法经常呈现在一些泛型工具库中。

extends 的语法格局简略总结如下,留神上面的类型既能够是个别意义上的类型也能够是泛型

泛型名 extends 类型

泛型条件

下面提到 extends,其实也能够当做一个三元运算符,如下

T extends U? X: Y

这里便不限度 T 肯定要是 U 的子类型,如果是 U 子类型,则将 T 定义为 X 类型,否则定义为 Y 类型。

留神,生成的后果是 调配式的

举个例子,如果咱们把 X 换成 T,如此模式:T extends U? T: never

此时返回的 T,是满足原来的 T 中蕴含 U 的局部,能够了解为 T 和 U 的 交加

所以,extends 的语法格局能够扩大为

泛型名 A extends 类型 B ? 类型 C: 类型 D 

泛型推断 infer

infer 的中文是“推断”的意思,个别是搭配下面的泛型条件语句应用的,所谓推断,就是你不必预先指定在泛型列表中,在运行时会主动判断,不过你得先预约义好整体的构造。举个例子

type Foo<T> = T extends {t: infer Test} ? Test: string

首选看 extends 前面的内容,{t: infer Test}能够看成是一个蕴含 t 属性类型定义 ,这个 t 属性 的 value 类型通过 infer 进行推断后会赋值给 Test 类型,如果泛型理论参数合乎 {t: infer Test} 的定义那么返回的就是 Test 类型,否则默认给缺省的 string 类型。

举个例子加深下了解

type One = Foo<number>  // string,因为 number 不是一个蕴含 t 的对象类型
type Two = Foo<{t: boolean}>  // boolean,因为泛型参数匹配上了,应用了 infer 对应的 type
type Three = Foo<{a: number, t: () => void}> // () => void,泛型定义是参数的子集,同样适配

infer用来对满足的泛型类型进行子类型的抽取,有很多高级的泛型工具也奇妙的应用了这个办法。

五、泛型工具

Partical<T>

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

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

举个例子,这个类型定义在上面也会用到

type Animal = {
  name: string,
  category: string,
  age: number,
  eat: () => number}

应用 Partical 包裹一下

type PartOfAnimal = Partical<Animal>;
const ww: PartOfAnimal = {name: 'ww'}; // 属性全副可选后,能够只赋值局部属性了

Record<K, T>

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

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

这里特地阐明一下,keyof any对应的类型为 number | string | symbol,也就是能够做对象键(业余说法叫索引 index) 的类型汇合。

举个例子

const obj: Record<string, string> = {'name': 'xiaoming', 'tag': '三好学生'}

Pick<T, K>

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

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

咱们还是用下面的 Animal 定义,看一下 Pick 如何应用

const bird: Pick<Animal, "name" | "age"> = {name: 'bird', age: 1}

Exclude<T, U>

此工具是在 T 类型中,去除 T 类型和 U 类型的交加,返回残余的局部

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

留神这里的 extends 返回的 T 是原来的 T 中和 U 无交加的属性,而任何属性联结 never 都是本身,具体可在上文查阅。

举个例子

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;   // "c"
type T2 = Exclude<string | number | (() => void), Function>; // string | number

Omit<T, K>

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

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

在定义中,第一步先从 T 的 key 中去掉与 K 重叠的 key,接着应用 Pick 把 T 类型和残余的 key 组合起来即可

还是用下面的 Animal 举个例子

const OmitAnimal:Omit<Animal, 'name'|'age'> = {category: 'lion', eat: () => {console.log('eat') } }

能够发现,Omit 与 Pick 失去的后果齐全相同,一个是取非后果,一个取交后果。

ReturnType<T>

此工具就是获取 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;

通过应用 infer 推断返回值类型,而后返回此类型,如果你彻底了解了 infer 的含意,那这段就很好了解

举个例子

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

Required<T>

此工具能够将类型 T 中所有的属性变为必选项

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

这里有一个很有意思的语法-?,你能够了解为就是 TS 中把? 可选属性减去的意思。

除了这些以外,还有很多的内置的类型工具,能够参考 TypeScript Handbook 取得更具体的信息,同时 Github 上也有很多第三方类型辅助工具,如 utility-types 等。

六、我的项目实战

这里分享一些我集体的想法,可能兴许会比拟全面甚至谬误,欢送大家踊跃留言探讨

Q: 偏好应用 interface 还是 type 来定义类型?

A: 从用法上来说两者实质上没有区别,大家应用 React 我的项目做业务开发的话,次要就是用来定义 Props 以及接口数据类型。

然而从扩大的角度来说,type 比 interface 更不便拓展一些,如果有以下两个定义

type Name = {name: string};
interface IName {name: string};

想要做类型的扩大的话,type 只须要一个&,而 interface 要多写不少代码

type Person = Name & {age: number};
interface IPerson extends IName {age: number};

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

不过 interface 有一个比拟弱小的中央就是能够反复定义增加属性,比方咱们须要给 window 对象增加一个自定义的属性或者办法,那么咱们间接基于其 Interface 新增属性就能够了。

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

总体来说,大家晓得 TS 是类型兼容而不是类型名称匹配的,所以个别不需用面向对象的场景或者不须要批改全局类型的场合,我个别都是用 type 来定义类型。

Q: 是否容许 any 类型的呈现

A: 说实话,刚开始应用 TS 的时候还是挺喜爱用 any 的,毕竟大家都是从 JS 过渡过去的,对这种影响效率的代码开发方式并不能齐全承受,因而不论是出于偷懒还是找不到适合定义的状况,应用 any 的状况都比拟多。

随着应用工夫的减少和对 TS 学习了解的加深,逐渐离不开了 TS 带来的类型定义红利,不心愿代码中呈现 any,所有类型都必须要一个一个找到对应的定义,甚至曾经丢失了裸写 JS 的勇气。

这是一个目前没有正确答案的问题,总是要在效率和工夫等等因素中找一个最适宜本人的均衡。不过我还是举荐应用 TS,随着前端工程化演进和位置的进步,强类型语言肯定是多人合作和代码强壮最牢靠的保障之一,多用 TS,少用 any,也是前端界的一个广泛共识。

Q: 类型定义文件 (.d.ts) 如何搁置

A: 这个如同业界也没有特地对立的标准,我的想法如下:

  • 长期的类型,间接在应用时定义

如本人写了一个组件外部的 Helper,函数的入参和出参只供外部应用也不存在复用的可能,能够间接在定义函数的时候就在前面定义

function format(input: {k: string}[]): number[] { /***/}
  • 组件个性化类型,间接定义在 ts(x)文件中

如 AntD 组件设计,每个独自组件的 Props、State 等专门定义了类型并 export 进来

// Table.tsx
export type TableProps = {/***/}
export type ColumnProps = {/***/}
export default function Table() { /***/}

这样使用者如果须要这些类型能够通过 import type 的形式引入来应用。

  • 范畴 / 全局数据,定义在.d.ts 文件中

全局类型数据,这个大家毫无异议,个别根目录下有个 typings 文件夹,外面会寄存一些全局类型定义。

如果咱们应用了 css module,那么咱们须要让 TS 辨认.less 文件 (或者.scss) 引入后是一个对象,能够如此定义

declare module '*.less' {const resource: { [key: string]: string };
  export = resource;
}

而对于一些全局的数据类型,如后端返回的通用的数据类型,我也习惯将其放在 typings 文件夹下,应用 Namespace 的形式来防止名字抵触,如此能够节俭组件 import 类型定义的语句

declare namespace EdgeApi {
  export interface Department {
    description: string;
    gmt_create: string;
    gmt_modify: string;
    id: number;
    name: string;
  }
}

这样,每次应用的时候,只须要 const department: EdgeApi.Department 即可,节俭了不少导入的精力。开发者只有能约定标准,防止命名抵触即可。

对于 TS 用法的总结就完结到这里,感激大家的观看~

The End

正文完
 0