关于 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.tsx
const Parent: FC = () => {
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>
运算符
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 FooTyp
e = ReturnType<foo>; // string | number
7. Parameters
此工具就是获取T类型(函数)对应的参数类型
type Parameters<T> = T extends (...args:string[]) => any ? string[] : any;
老规矩举个栗子
type Fn = (a: string, b: number) => void
type 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