前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >TypeScript 疑难杂症

TypeScript 疑难杂症

作者头像
ConardLi
发布2019-09-25 15:13:13
1.9K0
发布2019-09-25 15:13:13
举报
文章被收录于专栏:code秘密花园code秘密花园

作者:阿伟 - 身在高楼心在北大荒,我就这副死样~https://zhuanlan.zhihu.com/p/82459341

互斥类型

2019.09.19 新增

代码语言:javascript
复制
// https://github.com/Microsoft/TypeScript/issues/14094#issuecomment-373782604
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type XOR<T, U> = (T | U) extends object
  ? (Without<T, U> & U) | (Without<U, T> & T)
  : T | U

使用上面的 XOR范型,我们可以很容易地实现如下需求:

  • 如果类型是 A,那绝不可能是 B
代码语言:javascript
复制
// https://stackoverflow.com/questions/42123407/does-typescript-support-mutually-exclusive-types

interface Person {
  ethnicity: string
}
interface Pet {
  breed: string
}
function getOrigin(value: XOR<Person, Pet>) {}

getOrigin({}) //Error
getOrigin({ ethnicity: 'abc' }) //OK
getOrigin({ breed: 'def' }) //OK
getOrigin({ ethnicity: 'abc', breed: 'def' }) //Error

不要想当然的认为可以这样:Person | Pet

  • 某个对象中要不有属性a,要不有属性b,但二者不能同时都有

一个常见的例子是页面导航菜单组件的配置,如果包含了path就不可能包含children,偷懒的做法是:

代码语言:javascript
复制
type Option = { name: string, path?: string, children?: Option[] }

上面这个显然不够类型安全,而且在你解析该配置的时候也不够方便,比如,你不能这样:

代码语言:javascript
复制
if (option.children) doSthWithChildren(option.children)
else if (option.path) doSthWithPath(option.path) // 当不存在 children 的时候,这里也并不能自动推导出有 path,你必须得显式判断

利用上面的 XOR,可以这样:

代码语言:javascript
复制
type Option = {name: string} & XOR<{path: string}, {children: Option[]}>
// type Option = {name: string} & XOR<{path: string, icon: string}, {children: Option[]}> // 在增加个 icon 属性也是可以的

不要想当然的认为可以这样:{name: string, path: string} | {name: string, children: Option[]}

  • 某个对象中某些属性要不都有,要不就一个都别有

bc总是会成对出现,要不就不出现:

代码语言:javascript
复制
type Object = { a: string } & XOR<{}, { b: string, c: number }>
  • 大于 2 个的互斥类型该怎么做?
代码语言:javascript
复制
type Test = XOR<A, XOR<B, C>>

很难过,据我所知 TypeScript 还不支持"不定泛型",所以你没法让XOR可以支持这样:

代码语言:javascript
复制
type Test = XOR<A, B, C, ...>
  • XOR 的一个 bug ?

原因不明。。。

代码语言:javascript
复制
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type XOR<T, U> = (T | U) extends object
  ? (Without<T, U> & U) | (Without<U, T> & T)
  : T | U

type Option = { name: string } & XOR<{ path: string }, { children: Option[] }>

// const option: Option = {name: '1', path: ''} // ok
// const option: Option = {name: '1', children: []} // ok
// const option: Option = {name: '1', value: '', children: []} // error, but ok

declare const option: Option

if (option.children) option.children.concat()
else option.path.toLocaleUpperCase()

if (option.path) option.path.toLocaleUpperCase()
else option.children.concat() // Object is possibly 'undefined'!! why?

我知道原因的时候会回来更新,如果你知道的话也欢迎留言~

让 BindCallApply“更安全”

2019.09.19 新增

不知道你有没有注意过这个问题:

代码语言:javascript
复制
function test(a: number) {}
test.call(null, '1') // 你传入了一个 string,但并不报错

类似的还有bindapply都有这个问题。不过在 3.2版本后,你可以通过开启 strictBindCallApply来使你的 BindCallApply“更安全”。感兴趣的话还可以来看一下它的 实现。

限制传入对象必须包含某些字段

用于给某个处理特定对象的函数来限制传入参数,尤其是当对象的某些字段是可选项的时候,比如说: test函数接受的参数类型为:

代码语言:javascript
复制
interface Param {
  key1?: string
  key2?: string
}

这种情况下调用方传入 {}也可通过类型检测。如果对字段类型没有严格要求,只希望限制必须包含某些字段可以这么做:

代码语言:javascript
复制
type MustKeys = 'key1' | 'key2'
function test<T extends MustKeys extends keyof T ? any : never>(obj: T) {}
test({})
test({ key1: 'a' })
test({ key1: 'a', key2: 1 })

善用 infer 来做类型“解构”

比如你想获取某个被 Promise包装过的原类型

代码语言:javascript
复制
type PromiseInnerType<T extends Promise<any>> = T extends Promise<infer P>
  ? P
  : never
type Test = PromiseInnerType<Promise<string>> // Test 的类型为 string

其实上面就是 infer的通常用途。举一反四,任意工具类型都可以用上述原理来做类型“解构”。比如,你还可以写一个 ReturnType的 Promise 版本,用于获取 Promise 函数的"解构类型"

代码语言:javascript
复制
type PromiseReturnType<T extends () => any> = ReturnType<T> extends Promise<
  infer R
>
  ? R
  : ReturnType<T>

async function test() {
  return { a: 1 }
}

type Test = PromiseReturnType<typeof test> // Test 的类型为 { a: number }

如果你用 React 的话,不难想到可以利用上述原理从一个组件中获取 PropsType。不过,现在你可以直接使用 React.ComponentProps来拿到

将联合类型转成交叉类型

代码语言:javascript
复制
// https://stackoverflow.com/a/50375286/2887218
type UnionToIntersection<U> = (U extends any
  ? (k: U) => void
  : never) extends ((k: infer I) => void)
  ? I
  : never

// { a: string } | { b: number } => { a: string } & { b: number }
type Test = UnionToIntersection<{ a: string } | { b: number }>

将交叉类型“拍平”

这个一般用于希望调用方能得到更清爽的类型提示,比如你的某个函数的参数类型限制为:

代码语言:javascript
复制
type Param = { a: string } & { b: number } & { c: boolean }

在调用的时候,实时提示会巨长,不方便查看,所以你可以用如下工具类型:

代码语言:javascript
复制
type Prettify<T> = T extends infer U ? { [K in keyof U]: U[K] } : never
type Param = Prettify<{ a: string } & { b: number } & { c: boolean }>

Param将等同于如下类型:

代码语言:javascript
复制
type Param = { a: string; b: number; c: boolean }

从一个函数数组中获取所有函数返回值的合并类型

函数数组为:

代码语言:javascript
复制
function injectUser() {
  return { user: 1 }
}

function injectBook() {
  return { book: '2' }
}

const injects = [injectUser, injectBook]

如何实现一个工具类型来获取上面这个injects数组中每个函数返回值的合并类型呢?即: { user: number, book: string }

代码语言:javascript
复制
type UnionToIntersection<U> = (U extends any
  ? (k: U) => void
  : never) extends ((k: infer I) => void)
  ? I
  : never
type Prettify<T> = T extends infer U ? { [K in keyof U]: U[K] } : never
type InjectTypes<T extends Array<() => object>> = T extends Array<() => infer P>
  ? Prettify<UnionToIntersection<P>>
  : never

type Test = InjectTypes<typeof injects> // Test 的类型为 { user: number, book: string }

利用上面的原理,你可以很容易地实现这个需求:

实现一个 getInjectData 函数,它接受若干个函数参数,返回值为这些函数返回对象的合并结果

代码语言:javascript
复制
function getInjectData<T extends Array<() => any>>(injects: T): InjectTypes<T> {
  const data: any = {}
  for (let inject of injects) Object.assign(data, inject())
  return data
}
getInjectData([injectUser, injectBook]) // { user: number, book: string }

...args 函数不定参数 + 泛型

接着上面的getInjectData继续看,有个小缺点是你必须得给它传一个数组,而不能传不定参数(如果你用下面的方式实现的话:

代码语言:javascript
复制
function getInjectData<T extends () => any>(...injects: T[]): InjectTypes<T[]> {
  const data: any = {}
  for (let inject of injects) Object.assign(data, inject())
  return data
}
getInjectData(injectUser, injectBook) // Argument of type '() => { book: string; }' is not assignable to parameter of type '() => { user: number; }'

原因是 TypeScript 会取第一个参数作为 injects数组元素的类型。解决方案是:

代码语言:javascript
复制
function getInjectData<T extends Array<() => any>>(
  ...injects: T
): InjectTypes<T> {
  const data: any = {}
  for (let inject of injects) Object.assign(data, inject())
  return data
}
getInjectData(injectUser, injectBook) // { user: number, book: string }

原理是 TypeScript 会为使用了不定参数运算符的每个参数自动解包数组泛型和其一一映射

自己实现一个“完美的” Object.assign 类型

2019.09.21 新增

在你理解了上面的联合类型转成交叉类型...args 函数不定参数 + 泛型之后,我们可以尝试来“完善”一下 Object.assign 的类型。

这里之所以说完善它,原因是在你给该函数传入超过 4 个对象之后,它会返回 any,而不再是所有对象的交叉类型:

原因可以看下官方的类型实现:Object.assign

代码语言:javascript
复制
type UnionToIntersection<U> = (U extends any
  ? (k: U) => void
  : never) extends ((k: infer I) => void)
  ? I
  : never

function assign<T extends Array<any>>(
  ...args: T
): UnionToIntersection<T extends Array<infer P> ? P : never> {
  return {} as any
}

assign({ a: 1 }, { b: '2' }, { c: 3 }, { d: '4' }, { e: 5 })

引用某个类型的子类型

代码语言:javascript
复制
type Parent1 = { fun<T>(): T }
type Parent2 = { [key: string]: number }
type Child1 = Parent1['fun']
type Child2 = Parent2['']

但是好像没有办法直接“固化”上面fun函数的泛型,就像这样:

代码语言:javascript
复制
type Child1 = Parent1['fun']<number>

如果找到好办法我再来更新吧。。

动态更改链式调用的返回值类型

代码语言:javascript
复制
type Omit<T, K> = Pick<T, Exclude<keyof T, K>> // TypesScript 3.5 已自带

class Chain<T = number> {
  fun1() {
    return this as Omit<this, 'fun1'>
  }

  fun2(arg: T) {
    return (this as unknown) as Chain<string>
  }

  fun3() {
    return this
  }
}

如上代码,希望实现调用 fun1后,不能再调用 fun1,调用 fun2后,之后 fun2只允许传 string。

然后 测试一下会发现在调用完 fun2后,fun1又可以调用了,并不符合预期。怎么永久剔除掉 fun1呢?如下代码:

代码语言:javascript
复制
type Omit<T, K> = Pick<T, Exclude<keyof T, K>> // TypesScript 3.5 已自带

class Chain<T = number> {
  fun1<P extends Partial<Chain<T>>>(this: P) {
    return this as Omit<P, 'fun1'>
  }

  fun2<P extends Partial<Chain<T>>>(this: P, arg: T) {
    return (this as unknown) as Pick<
      Chain<string>,
      Extract<keyof P, keyof Chain<string>>
    >
  }

  fun3<P>(this: P) {
    return this
  }
}

不过坏处是所有对外的方法你都得显示声明一遍 this的类型才可以维持这份 "动态类型"。尤其是在方法内部需要调用 this 中的其他数据时候,往往得各种 as any。如果你有更好的办法,欢迎留言~

操作两个字段相近的对象

代码语言:javascript
复制
const obj1 = { a: 1, b: 2, c: 3 }
const obj2 = { a: 1, b: 2, d: 3 }
const keys = ['a', 'b']
for (let key of keys) {
  console.log(obj1[key])
  console.log(obj2[key])
}

上述代码在开启了 noImplicitAny后是会报错的,那除了 @ts-ignore如何正确地声明 keys的类型呢?如下代码:

代码语言:javascript
复制
const obj1 = { a: 1, b: 2, c: 3 }
const obj2 = { a: 1, b: 2, d: 3 }
const keys: Array<keyof typeof obj1 & keyof typeof obj2> = ['a', 'b']

这个问题相对简单些,当时想着 @ts-ignore能支持 block 就好了,可惜目前为止还没支持,可以关注这个 issue

让对象的类型就是它的“字面类型”

代码语言:javascript
复制
const obj = { a: 1, b: '2' }

上面的代码显然 obj的类型会被自动推导成:

代码语言:javascript
复制
interface Obj {
  a: number
  b: string
}

但有时候希望 obj的类型就是它的字面意思:

代码语言:javascript
复制
interface Obj {
  a: 1
  b: '2'
}

可以这样:

代码语言:javascript
复制
const obj = { a: 1, b: '2' } as const

const assertions

如何让泛型不被自动推导,让泛型为必填项

代码语言:javascript
复制
class Test<P> {
  constructor(data: P) {}
}
new Test({ a: 1 }) // P 会被自动推导成 { a: number }

有些时候我们希望别人在使用 Test这个类的时候必须得显式传入一个泛型才能使用。可以这样:

代码语言:javascript
复制
type NoInfer<T> = [T][T extends any ? 0 : never]
class Test<P = void> {
  constructor(
    data: NoInfer<P> & (P extends void ? 'You have to pass in a generic' : {}),
  ) {}
}

new Test({ a: ' ' }) // err Argument of type '{ a: string; }' is not assignable to parameter of type 'void & "You have to pass in a generic"'.
new Test<{ a: string }>({ a: ' ' }) // ok

利用 NoInfer这个工具类,泛型函数也同理:

代码语言:javascript
复制
type NoInfer<T> = [T][T extends any ? 0 : never]
function test<P = void>(
  data: NoInfer<P> & (P extends void ? 'You have to pass in a generic' : {}),
) {}

test(1)
test<number>(1)
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-09-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 code秘密花园 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 让 BindCallApply“更安全”
  • 限制传入对象必须包含某些字段
  • 善用 infer 来做类型“解构”
  • 将联合类型转成交叉类型
  • 将交叉类型“拍平”
  • 从一个函数数组中获取所有函数返回值的合并类型
  • ...args 函数不定参数 + 泛型
  • 自己实现一个“完美的” Object.assign 类型
  • 动态更改链式调用的返回值类型
  • 操作两个字段相近的对象
  • 让对象的类型就是它的“字面类型”
  • 如何让泛型不被自动推导,让泛型为必填项
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档