本文主要介绍 TypeScript 的高级用法,适用于对 TypeScript 已经有所了解或者已经实际用过一段时间的同窗,分别从类型、运算符、操做符、泛型的角度来系统介绍常见的 TypeScript 文章没有好好讲解的功能点,最后再分享一下本身的实践经历。css
unknown 指的是不可预先定义的类型,在不少场景下,它能够替代 any 的功能同时保留静态检查的能力。html
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;
any.substr(1); // Pass: any类型至关于放弃了静态检查
复制代码
unknown 的一个使用场景是,避免使用 any 做为函数的参数类型而致使的静态类型检查 bug:git
function test(input: unknown): number {
if (Array.isArray(input)) {
return input.length; // Pass: 这个代码块中,类型守卫已经将input识别为array类型
}
return input.length; // Error: 这里的input仍是unknown类型,静态检查报错。若是入参是any,则会放弃检查直接成功,带来报错风险
}
复制代码
在 TS 中,void 和 undefined 功能高度相似,能够在逻辑上避免不当心使用了空指针致使的错误。github
function foo() {} // 这个空函数没有返回任何值,返回类型缺省为void
const a = foo(); // 此时a的类型定义为void,你也不能调用a的任何属性方法
复制代码
void 和 undefined 类型最大的区别是,你能够理解为 undefined 是 void 的一个子集,当你对函数返回值并不在乎时,使用 void 而不是 undefined。举一个 React 中的实际的例子。typescript
// 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等均可以
}
function Child({ getValue }: Props) => <div>{getValue()}</div>
复制代码
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 有以下特性:数组
deadcode
function test() {
foo(); // 这里的foo指上面返回never的函数
console.log(111); // Error: 编译器报错,此行代码永远不会执行到
}
复制代码
let n: never;
let 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 时,才会返回右侧表达式 。
好比咱们书写了let b = a ?? 10
,生成的代码以下:
let b = a !== null && a !== void 0 ? a : 10;
复制代码
而 || 表达式,你们知道的,则对 false、''、NaN、0 等逻辑空值也会生效,不适于咱们作对参数的合并。
let num:number = 1_2_345.6_78_9
复制代码
_能够用来对长数字作任意的分隔,主要设计是为了便于数字的阅读,编译出来的代码是没有下划线的,请放心食用。
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 是获取一个对象/实例的类型,以下:
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 只能用在类型的定义中,能够对枚举类型进行遍历,以下:
// 这个类型能够将任何类型的键值转化成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 的中文是“推断”的意思,通常是搭配上面的泛型条件语句使用的,所谓推断,就是你不用预先指定在泛型列表中,在运行时会自动判断,不过你得先预约义好总体的结构。举个例子
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
用来对知足的泛型类型进行子类型的抽取,有不少高级的泛型工具也巧妙的使用了这个方法。
此工具的做用就是将泛型中所有属性变为可选的。
type Partial<T> = {
[P in keyof T]?: T[P]
}
复制代码
举个例子,这个类型定义在下面也会用到。
type Animal = {
name: string,
category: string,
age: number,
eat: () => number
}
复制代码
使用 Partial 包裹一下。
type PartOfAnimal = Partial<Animal>;
const ww: PartOfAnimal = { name: 'ww' }; // 属性所有可选后,能够只赋值部分属性了
复制代码
此工具的做用是将 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': 'zhangsan', 'tag': '打工人' }
复制代码
此工具的做用是将 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 }
复制代码
此工具是在 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
复制代码
此工具可认为是适用于键值对对象的 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 获得的结果彻底相反,一个是取非结果,一个取交结果。
此工具就是获取 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
复制代码
此工具能够将类型 T 中全部的属性变为必选项。
type Required<T> = {
[P in keyof T]-?: T[P]
}
复制代码
这里有一个颇有意思的语法-?
,你能够理解为就是 TS 中把?可选属性减去的意思。
除了这些之外,还有不少的内置的类型工具,能够参考TypeScript Handbook得到更详细的信息,同时 Github 上也有不少第三方类型辅助工具,如utility-types等。
这里分享一些我我的的想法,可能也许会比较片面甚至错误,欢迎你们积极留言讨论
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 来定义类型。
A: 说实话,刚开始使用 TS 的时候仍是挺喜欢用 any 的,毕竟你们都是从 JS 过渡过来的,对这种影响效率的代码开发方式并不能彻底接受,所以无论是出于偷懒仍是找不到合适定义的状况,使用 any 的状况都比较多。
随着使用时间的增长和对 TS 学习理解的加深,逐步离不开了 TS 带来的类型定义红利,不但愿代码中出现 any,全部类型都必需要一个一个找到对应的定义,甚至已经丧失了裸写 JS 的勇气。
这是一个目前没有正确答案的问题,老是要在效率和时间等等因素中找一个最适合本身的平衡。不过我仍是推荐使用 TS,随着前端工程化演进和地位的提升,强类型语言必定是多人协做和代码健壮最可靠的保障之一,多用 TS,少用 any,也是前端界的一个广泛共识。
A: 这个好像业界也没有特别统一的规范,个人想法以下:
如本身写了一个组件内部的 Helper,函数的入参和出参只供内部使用也不存在复用的可能,能够直接在定义函数的时候就在后面定义。
function format(input: {k: string}[]): number[] { /***/ }
复制代码
如 AntD 组件设计,每一个单独组件的 Props、State 等专门定义了类型并 export 出去。
// Table.tsx
export type TableProps = { /***/ }
export type ColumnProps = { /***/ }
export default function Table() { /***/ }
复制代码
这样使用者若是须要这些类型能够经过 import type 的方式引入来使用。
全局类型数据,这个你们毫无异议,通常根目录下有个 typings 文件夹,里面会存放一些全局类型定义。
假如咱们使用了 css module,那么咱们须要让 TS 识别.less 文件(或者.scss)引入后是一个对象,能够如此定义:
declare module '*.less' {
const resource: { [key: string]: string };
export = resource;
}
复制代码
而对于一些全局的数据类型,如后端返回的通用的数据类型,我也习惯将其放在 typings 文件夹下,使用 Namespace 的方式来避免名字冲突,如此能够节省组件 import 类型定义的语句。
declare namespace EdgeApi {
interface Department {
description: string;
gmt_create: string;
gmt_modify: string;
id: number;
name: string;
}
}
复制代码
这样,每次使用的时候,只须要const department: EdgeApi.Department
便可,节省了很多导入的精力。开发者只要能约定规范,避免命名冲突便可。
关于 TS 用法的总结就介绍到这里,感谢你们的观看~
欢迎关注「 字节前端 ByteFE 」
简历投递联系邮箱「 tech@bytedance.com 」