本文会不定期更新,我遇到的觉得值得分享的 TypeScript 问题都会写在这里
如果你有一些问都不知道咋问的 TypeScript 问题,来这里翻翻或许能找到答案!
互斥类型
2019.09.19 新增
1 | // https://github.com/Microsoft/TypeScript/issues/14094#issuecomment-373782604 |
使用上面的 XOR 范型,我们可以很容易地实现如下需求:
如果类型是 A,那绝不可能是 B
1 | // https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types |
不要想当然的认为可以这样: Person | Pet
某个对象中要不有属性 a
,要不有属性 b
,但二者不能同时都有。
一个常见的例子是页面导航菜单组件的配置,如果包含了 path
就不可能包含 children
,偷懒的做法是:
1 | type Option = { name: string; path?: string; children?: Option[] } |
上面这个显然不够类型安全,而且在你解析该配置的时候也不够方便,比如,你不能这样:
1 | if (option.children) doSthWithChildren(option.children) |
利用上面的 XOR
,可以这样:
1 | type Option = {name: string} & XOR<{path: string}, {children: Option[]}> |
不要想当然的认为可以这样:{name: string, path: string} | {name: string, children: Option[]}
某个对象中某些属性要不都有,要不就一个都别有
b
和 c
总是会成对出现,要不就不出现:
1 | type Object = { a: string } & XOR<{}, { b: string; c: number }> |
大于 2 个的互斥类型该怎么做?
1 | type Test = XOR<A, XOR<B, C>> |
很难过,据我所知 TypeScript 还不支持”不定泛型”,所以你没法让 XOR
可以支持这样:
1 | type Test = XOR<A, B, C, ...> |
XOR 的一个 bug ?
1 | type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never } |
感谢 @残血面包 的解释:option.path
是 ''
的话也会进入 else 条件。
1 | // 这样就没问题了 |
让 BindCallApply “更安全”
2019.09.19 新增
不知道你有没有注意过这个问题:
1 | function test(a: number) {} |
类似的还有bind
apply
都有这个问题。不过在 3.2
版本后,你可以通过开启 strictBindCallApply 来使你的 BindCallApply “更安全”。感兴趣的话还可以来看一下它的实现
限制传入对象必须包含某些字段
用于给某个处理特定对象的函数来限制传入参数,尤其是当对象的某些字段是可选项的时候,比如说:test
函数接受的参数类型为:
1 | interface Param { |
这种情况下调用方传入 {}
也可通过类型检测。如果对字段类型没有严格要求,只希望限制必须包含某些字段可以这么做:
1 | type MustKeys = 'key1' | 'key2' |
善用 infer 来做类型“解构”
比如你想获取某个被 Promise
包装过的原类型
1 | type PromiseInnerType<T extends Promise<any>> = T extends Promise<infer P> |
其实上面就是 infer 的通常用途。举一反四,任意工具类型都可以用上述原理来做类型“解构”。比如,你还可以写一个 ReturnType
的 Promise 版本,用于获取 Promise 函数的”解构类型”
1 | type PromiseReturnType<T extends () => any> = ReturnType<T> extends Promise< |
如果你用 React 的话,不难想到可以利用上述原理从一个组件中获取 PropsType。不过,现在你可以直接使用 React.ComponentProps 来拿到
将联合类型转成交叉类型
1 | // https://stackoverflow.com/a/50375286/2887218 |
将交叉类型“拍平”
这个一般用于希望调用方能得到更清爽的类型提示,比如你的某个函数的参数类型限制为:
1 | type Param = { a: string } & { b: number } & { c: boolean } |
在调用的时候,实时提示会巨长,不方便查看,所以你可以用如下工具类型:
1 | type Prettify<T> = T extends infer U ? { [K in keyof U]: U[K] } : never |
Param
将等同于如下类型:
1 | type Param = { a: string; b: number; c: boolean } |
从一个函数数组中获取所有函数返回值的合并类型
函数数组为:
1 | function injectUser() { |
如何实现一个工具类型来获取上面这个injects
数组中每个函数返回值的合并类型呢?即:{ user: number, book: string }
1 | type UnionToIntersection<U> = (U extends any |
利用上面的原理,你可以很容易地实现这个需求:
实现一个 getInjectData 函数,它接受若干个函数参数,返回值为这些函数返回对象的合并结果
1 | function getInjectData<T extends Array<() => any>>(injects: T): InjectTypes<T> { |
顺便安利一下 express-action 简洁优雅地处理你的 express 请求,它的 injectData
就是利用了该原理。
…args 函数不定参数 + 泛型
接着上面的getInjectData
继续看,有个小缺点是你必须得给它传一个数组,而不能传不定参数(如果你用下面的方式实现的话:
1 | function getInjectData<T extends () => any>(...injects: T[]): InjectTypes<T[]> { |
原因是 TypeScript 会取第一个参数作为 injects
数组元素的类型。解决方案是:
1 | function getInjectData<T extends Array<() => any>>( |
原理是 TypeScript 会为使用了不定参数运算符的每个参数自动解包数组泛型和其一一映射
自己实现一个“完美的” Object.assign 类型
2019.09.21 新增
在你理解了上面的联合类型转成交叉类型和…args 函数不定参数 + 泛型 之后,我们可以尝试来“完善”一下 Object.assign 的类型。
这里之所以说完善它,原因是在你给该函数传入超过 4 个对象之后,它会返回 any,而不再是所有对象的交叉类型:在线测试
原因可以看下官方的类型实现:Object.assign
1 | type UnionToIntersection<U> = (U extends any |
引用某个类型的子类型
1 | type Parent1 = { fun<T>(): T } |
但是好像没有办法直接“固化”上面fun
函数的泛型,就像这样:
1 | type Child1 = Parent1['fun']<number> |
如果找到好办法我再来更新吧。。
泛型 & 子类型联动
2021.09.11 新增
见文章:TypeScript:我都传了 type 了,能不能给我自动推导出 data 类型啊?
实现一个类型完备的事件通讯器
2021.09.15 新增
见文章:用 TS 实现一个类型完备的事件通讯器居然有两种方式
动态更改链式调用的返回值类型
1 | type Omit<T, K> = Pick<T, Exclude<keyof T, K>> // TypesScript 3.5 已自带 |
如上代码,希望实现调用 fun1
后,不能再调用 fun1
,调用 fun2
后,之后 fun2
只允许传 string。
然后 测试一下 会发现在调用完 fun2
后,fun1
又可以调用了,并不符合预期。怎么永久剔除掉 fun1
呢?如下代码:
1 | type Omit<T, K> = Pick<T, Exclude<keyof T, K>> // TypesScript 3.5 已自带 |
不过坏处是所有对外的方法你都得显示声明一遍 this
的类型才可以维持这份 “动态类型”。尤其是在方法内部需要调用 this 中的其他数据时候,往往得各种 as any。如果你有更好的办法,欢迎留言~
操作两个字段相近的对象
1 | const obj1 = { a: 1, b: 2, c: 3 } |
上述代码在开启了 noImplicitAny 后是会报错的,那除了 @ts-ignore
如何正确地声明 keys
的类型呢?如下代码:
1 | const obj1 = { a: 1, b: 2, c: 3 } |
这个问题相对简单些,当时想着 @ts-ignore
能支持 block 就好了,可惜目前为止还没支持,可以关注这个 issue
让对象的类型就是它的“字面类型”
1 | const obj = { a: 1, b: '2' } |
上面的代码显然 obj
的类型会被自动推导成:
1 | interface Obj { |
但有时候希望 obj
的类型就是它的字面意思:
1 | interface Obj { |
可以这样:
1 | const obj = { a: 1, b: '2' } as const |
如何让泛型不被自动推导,让泛型为必填项
1 | class Test<P> { |
有些时候我们希望别人在使用 Test
这个类的时候必须得显式传入一个泛型才能使用。可以这样:
1 | // https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-504042546 |
利用 NoInfer
这个工具类,泛型函数也同理:
1 | type NoInfer<T> = [T][T extends any ? 0 : never] |
2019.10.09 新增
3.6 之后 ts 更新了类型推导,上面的这种写法不会提示你 You have to pass in a generic 了,影响不是很大,毕竟这个提示的实现本身是比较 ugly 的…
就直接这样吧(如果你有更好的办法欢迎留言):
1 | type NoInfer<T> = [T][T extends any ? 0 : never] |