Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TypeScript extends 精读与实践 #140

Closed
MuYunyun opened this issue Oct 4, 2021 · 14 comments
Closed

TypeScript extends 精读与实践 #140

MuYunyun opened this issue Oct 4, 2021 · 14 comments

Comments

@MuYunyun
Copy link
Owner

MuYunyun commented Oct 4, 2021

TypeScript extends 精读与实践

在大多数程序中,我们必须根据输入做出决策。TypeScript 也不例外,使用条件类型可以描述输入类型与输出类型之间的关系。

用于条件判断时的 extends

当 extends 表示条件判断时,可以总结出以下规律

  1. 若位于 extends 两侧的类型相同,则 extends 在语义上可理解为 ===,可以参考如下例子:
type result1 = 'a' extends 'abc' ? true : false // false
type result2 = 123 extends 1 ? true : false     // false
  1. 若位于 extends 右侧的类型包含位于 extends 左侧的类型(即狭窄类型 extends 宽泛类型)时,结果为 true,反之为 false。可以参考如下例子:
type result3 = string extends string | number ? true : false // true
  1. 当 extends 作用于对象时,若在对象中指定的 key 越多,则其类型定义的范围越狭窄。可以参考如下例子:
type result4 = { a: true, b: false } extends { a: true } ? true : false // true

在泛型类型中使用 extends

考虑如下 Demo 类型定义:

type Demo<T, U> = T extends U ? never : T

结合用于条件判断时的 extends,可知 'a' | 'b' | 'c' extends 'a' 是 false, 因此 Demo<'a' | 'b' | 'c', 'a'> 结果是 'a' | 'b' | 'c' 么?

查阅官网,其中有提到:

When conditional types act on a generic type, they become distributive when given a union type.

即当条件类型作用于泛型类型时,联合类型会被拆分使用。即 Demo<'a' | 'b' | 'c', 'a'> 会被拆分为 'a' extends 'a''b' extends 'a''c' extends 'a'。用伪代码表示类似于:

function Demo(T, U) {
  return T.map(val => {
    if (val !== U) return val
    return 'never'
  })
}

Demo(['a', 'b', 'c'], 'a') // ['never', 'b', 'c']

此外根据 never 类型的定义 —— never 类型可分配给每种类型,但是没有类型可以分配给 never(除了 never 本身)。即 never | 'b' | 'c' 等价于 'b' | 'c'

因此 Demo<'a' | 'b' | 'c', 'a'> 的结果并不是 'a' | 'b' | 'c' 而是 'b' | 'c'

工具类型

心细的读者可能已经发现了 Demo 类型的声明过程其实就是 TypeScript 官方提供的工具类型中 Exclude<Type, ExcludedUnion> 的实现原理,其用于将联合类型 ExcludedUnion 排除在 Type 类型之外。

type T = Demo<'a' | 'b' | 'c', 'a'> // T: 'b' | 'c'

基于 Demo 类型定义,进一步地还可以实现官方工具类型中的 Omit<Type, Keys>,其用于移除对象 Type
中满足 keys 类型的属性值。

type Omit<Type, Keys> = {
  [P in Demo<keyof Type, Keys>]: Type<P>
}

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type T = Omit<Todo, 'description'> // T: { title: string; completed: boolean }

逃离舱

如果想让 Demo<'a' | 'b' | 'c', 'a'> 的结果为 'a' | 'b' | 'c' 是否可以实现呢? 根据官网描述:

Typically, distributivity is the desired behavior. To avoid that behavior, you can surround each side of the extends keyword with square brackets.

如果不想遍历泛型中的每一个类型,可以用方括号将泛型给括起来以表示使用该泛型的整体部分。

type Demo<T, U> = [T] extends [U] ? never : T

// result 此时类型为 'a' | 'b' | 'c'
type result = Demo<'a' | 'b' | 'c', 'a'>

在箭头函数中使用 extends

在箭头函数中使用三元表达式时,从左向右的阅读习惯导致函数内容区若不加括号则会让使用方感到困惑。比如下方代码中 x 是函数类型还是布尔类型呢?

// The intent is not clear.
var x = a => 1 ? true : false

在 eslint 规则 no-confusing-arrow 中,推荐如下写法:

var x = a => (1 ? true : false)

在 TypeScript 的类型定义中,若在箭头函数中使用 extends 也是同理,由于从左向右的阅读习惯,也会导致阅读者对类型代码的执行顺序感到困惑。

type Curry<P extends any[], R> =
  (arg: Head<P>) => HasTail<P> extends true ? Curry<Tail<P>, R> : R

因此在箭头函数中使用 extends 建议加上括号,对于进行 code review 有很大的帮助。

type Curry<P extends any[], R> =
  (arg: Head<P>) => (HasTail<P> extends true ? Curry<Tail<P>, R> : R)

结合类型推导使用 extends

在 TypeScript 中,一般会结合 extends 来使用类型推导 infer 语法。使用它可以实现自动推导类型的目的。比如用其来实现工具类型 ReturnType<Type>,该工具类型用于返回函数 Type 的返回类型。

type ReturnType<T extends Function> = T extends (...args: any) => infer U ? U : never

MyReturnType<() => string>          // string
MyReturnType<() => Promise<boolean> // Promise<boolean>

结合 extends 与类型推导还可以实现与数组相关的 Pop<T>Shift<T>Reverse<T> 工具类型。

Pop<T>:

type Pop<T extends any[]> = T extends [...infer ExceptLast, any] ? ExceptLast : never

type T = Pop<[3, 2, 1]> // T: [3, 2]

Shift<T>:

type Shift<T extends any[]> = T extends [infer _, ...infer O] ? O : never

type T = Shift<[3, 2, 1]> // T: [2, 1]

Reverse<T>

type Reverse<T> = T extends [infer F, ...infer Others]
  ? [...Reverse<Others>, F]
  : []

type T = Reverse<['a', 'b']> // T: ['b', 'a']

使用 extends 来判断两个类型完全相等

我们也可以使用 extends 来判断 A、B 两个类型是否完全相等。当前社区上主要有两种方案:

方案一: 参考 issue

export type Equal1<T, S> =
  [T] extends [S] ? (
    [S] extends [T] ? true : false
  ) : false

目前该方案的唯一缺点是会将 any 类型与其它任何类型判为相等。

type T = Equal1<{x:any}, {x:number}> // T: true

方案二: 参考 issue

export type Equal2<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<U>() => U extends Y ? 1 : 2) ? true : false

目前该方案的唯一缺点是在对交叉类型的处理上有一点瑕疵。

type T = Equal2<{x:1} & {y:2}, {x:1, y:2}> // false

以上两种判断类型相等的方法见仁见智,笔者在此抛砖引玉。

@kingyaroglek
Copy link

你好,

type A = { a: string }
type B = { readonly a: string }
type C = Equal1<A, B> // true
type D = Equal2<A, B> // false

这个要怎么解释呀

@MuYunyun
Copy link
Owner Author

你好,

type A = { a: string }
type B = { readonly a: string }
type C = Equal1<A, B> // true
type D = Equal2<A, B> // false

这个要怎么解释呀

Equal1 的判断方式相对宽松,Equal2 的判断方式更为严谨。这个 case 单从语义的结果上说,Equal2 应该是符合我们预期的。。

@cadleo
Copy link

cadleo commented Nov 8, 2021

云云牛批,云云能否解释下Equal2的工作原理,以前有个大佬在知乎回答过,但现在他删答案了,找不到了

@MuYunyun
Copy link
Owner Author

MuYunyun commented Nov 8, 2021

云云牛批,云云能否解释下Equal2的工作原理,以前有个大佬在知乎回答过,但现在他删答案了,找不到了

细节可以参考这篇 How does the Equals work in typescript?,暂时没有精力深究 ts 源码 😄

@stale
Copy link

stale bot commented Jan 8, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added wontfix and removed wontfix labels Jan 8, 2022
@stale
Copy link

stale bot commented Mar 15, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Mar 15, 2022
@AisonSu
Copy link

AisonSu commented Mar 23, 2022

云云牛批,云云能否解释下Equal2的工作原理,以前有个大佬在知乎回答过,但现在他删答案了,找不到了

https://juejin.cn/post/7078208046283014158

@stale stale bot removed the wontfix label Mar 23, 2022
@cadleo
Copy link

cadleo commented Mar 24, 2022

云云牛批,云云能否解释下Equal2的工作原理,以前有个大佬在知乎回答过,但现在他删答案了,找不到了

https://juejin.cn/post/7078208046283014158

我想知道的是这个实现中,为什么 函数类型 extends 函数类型时,两个函数类型会被判定为必须相等才返回true,如果按你的解释,其实把泛型T去掉,换成any就行了, 但是换成any是不行的

@stale
Copy link

stale bot commented May 25, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label May 25, 2022
@AisonSu
Copy link

AisonSu commented May 26, 2022

云云牛批,云云能否解释下Equal2的工作原理,以前有个大佬在知乎回答过,但现在他删答案了,找不到了

https://juejin.cn/post/7078208046283014158

我想知道的是这个实现中,为什么 函数类型 extends 函数类型时,两个函数类型会被判定为必须相等才返回true,如果按你的解释,其实把泛型T去掉,换成any就行了, 但是换成any是不行的

那只是一个例子,请仔细理解这句话:

在本例中,因为不知道T和X是否会相同,它所以变量x的类型将包括是X和不是X的情况,强制返回1|2——而类型1|2不能赋值给类型 Z extends Y ? 1 : 2,因为Z和T一样,都不知道是否会能被(Z所对应的)Y和(T所对应的)X所约束。

@cadleo
Copy link

cadleo commented May 29, 2022

云云牛批,云云能否解释下Equal2的工作原理,以前有个大佬在知乎回答过,但现在他删答案了,找不到了

https://juejin.cn/post/7078208046283014158

我想知道的是这个实现中,为什么 函数类型 extends 函数类型时,两个函数类型会被判定为必须相等才返回true,如果按你的解释,其实把泛型T去掉,换成any就行了, 但是换成any是不行的

那只是一个例子,请仔细理解这句话:

在本例中,因为不知道T和X是否会相同,它所以变量x的类型将包括是X和不是X的情况,强制返回1|2——而类型1|2不能赋值给类型 Z extends Y ? 1 : 2,因为Z和T一样,都不知道是否会能被(Z所对应的)Y和(T所对应的)X所约束。

首先感谢你的热心,其次我觉得你先解答一下我之前那条回复的疑问,再让我仔细理解你那句需要我通读多次再找到需要无视的冗余文字才能正常理解的话更好一点(实际上我再次仔细理解之后,依然无法解释为什么换成any之后,结果不如预期,你的“那只是一个例子”,给我的感觉好像是你觉得你的解释不需要覆盖所有的例子?当然也可能是我cpu不大行?)。最后,我再强调一下我的根本问题:为什么 函数类型 extends 函数类型时,两个函数类型会被判定为必须相等才返回true,我的意思是ts内部实现的时候,出于什么原因考虑专门这么做

@stale stale bot removed the wontfix label May 29, 2022
@HeavenSky
Copy link

image

这里是哪里出错了吗? 为啥我的不管怎么改都是Equal2 = true

@stale
Copy link

stale bot commented Sep 22, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Sep 22, 2022
@stale stale bot closed this as completed Sep 30, 2022
@yp910108
Copy link

yp910108 commented Aug 1, 2023

您好,方案二改成这样行不行:

type ParseFun<U> = <T>() => T extends U ? 1 : 2
type Equal2<X, Y> = ParseFun<X> extends ParseFun<Y> ? true : false
type T = Equal2<{x:1} & {y:2}, {x:1, y:2}> // true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants