前言
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