[Day09] TS:什麽!型别也有分配律?理解 Extract 和 Exclude 的实作

Distributive Conditional Types

上面这个是今天会提到的内容,如果你已经可以轻松看懂,欢迎直接左转去看我同事的精彩文章 — 「From State Machine to XState」!

前几天笔者提到的 Utility Types 多半是在 TypeScript 官方文件中提到的说明,但其实在 TypeScript 中也内建了一些 Utility Types,使用者不需要额外定义这些 Utility Types 就可以直接使用,这些内建的 Utility Types 列在官方网站的 references 中,今天就来让看其中两个内建的 Utility Types,分别是 ExcludeExtract

Extract 和 Exclude 的基本用法

即使我们还不了解 ExtractExclude 是怎麽被写出来的,但可以直接使用它,就好像有时不了解某个功能是如何被实作出来的,还是可以直接呼叫它提供的方法或函式一样。

还记得我们前面提到了解 Utility Types 的一个小技巧就是实际带入一个型别,把它会回传的内容存成一个 Type Alias 来看看吗。让我们先来看 Extract<Type, Union> 的使用:

// https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union

type T1 = Extract<'a' | 'b' | 'c', 'a'>; //  'a'
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'b'>; // 'a' | 'b'
type T3 = Extract<string | number | (() => void), Function>; // () => void
type T4 = Extract<'a' | 'b' | 'c', 'a' | 'f'>; // 'a'

可以看到 Extract 需要接受两个参数 TypeUnion,但它会做的是把 Type 中满足 Union 的取出,其余不满足的摒除掉,所以在:

  • 第一个例子中,从型别 'a' | 'b' | 'c' 中留下满足 'a' 的,所以最後得到 a
  • 第二个例子中,从型别 'a' | 'b' | 'c' 中留下满足 'a' | 'b' 的,所以最後得到 a | b
  • 第三和第四个例子也是一样的概念。

接着先来看 Exclude<Type, ExcludedUnion> 的使用:

// https://www.typescriptlang.org/docs/handbook/utility-types.html#excludetype-excludedunion

type T1 = Exclude<'a' | 'b' | 'c', 'a'>; //  'b' | 'c'
type T2 = Exclude<'a' | 'b' | 'c', 'a' | 'b'>; // 'c'
type T3 = Exclude<string | number | (() => void), Function>; // string | number
type T4 = Exclude<'a' | 'b' | 'c', 'a' | 'f'>; // 'b' | 'c

Exclude 的作用刚好和 Extract 相反,Exclude 虽然一样需要提供两个参数 TypeExcludedUnion,但它会做的是把 Type 中满足 ExcludedUnion 的剔除。所以在:

  • 第一个例子中,从型别 'a' | 'b' | 'c' 中剔除 a 後,只会剩下 'b' | 'c'
  • 第二个例子中,从型别 'a' | 'b' | 'c' 中剔除 'a' | 'b' 後,只会剩下 'c'
  • 第三和第四个例子也是一样的概念。

在知道了它们各种的用法後让我们来看它们的实作。

Conditional Types 的分配律(Distributive Conditional Types)

让我们先来看 Extract 的实作:

Distributive Conditional Types

这里用到了我们昨天提到的 Conditional Types 的观念,读者应该可以理解到原始码的意思就是:

「如果 T 是 U 的子集合,就回传 T,否则回传 never」

虽然我们已经理解了 Conditional Types,翻成白话文也完全正确,但在看到刚刚使用的范例是,却好像觉得少了什麽,思路无法连贯:

type T1 = Extract<'a' | 'b' | 'c', 'a'>; //  'a'

不是说如果 'a' | 'b' | 'c'(T)满足 'a'(U)的话,会直接回传 'a' | 'b' | 'c'(T)吗?为什麽最後只回传了 'a' 呢?

这里我们就要来提一下 Conditional Types 的分配律。「分配律」这个词有一种熟悉但有离了很遥远的感觉,但基本上我们一定都用过,例如:

a * (b + c) = a * b + a * c

上面这个就是乘法分配律。那麽什麽是 Conditional Types 的分配律呢?

假设说我们在 Utility Type 的泛型中带入的不是单纯一个型别,而是一个 union type 时会有分配律的情况产生。举例来说,先定义一个用 Conditional Type 写的 Utility Type:

// 定义一个用 Conditional Type 写的 Utility Type
type DistributeUnion<T> = T extends any ? T : never;

接着在 T 的地方带入 union type,像是这样:

type DistributeUnionReturn = DistributeUnion<'a' | 'b' | 'c'>; // "a" | "b" | "c"

这麽写的意思实际上等同於:

type DistributeUnionReturn =
  | DistributeUnion<'a'>
  | DistributeUnion<'b'>
  | DistributeUnion<'c'>;

也就是说原本的 'a' | 'b' | 'c' 会被分配到每个 DistributeUnion<T>T 中在用联集 | 起来,因为

  • DistributeUnion<'a'> 满足 any,所以会直接回传 a
  • DistributeUnion<'b'> 满足 any,所以会直接回传 b
  • DistributeUnion<'c'> 满足 any,所以会直接回传 c

最後就会等同於:

type DistributeUnionReturn = 'a' | 'b' | 'c';

这也就是为什麽,最终的回传值会是 'a' | 'b' | 'c'的缘故。

让我们把它放在一起看:

Distributive Conditional Types

回头来看 Extract 和 Exclude 的原始码

Extract 的原始码

理解了 Distributive Conditional Types 後,再让我们回头看 Extract 这个 Utility Type 的实作:

Distributive Conditional Types

现在应该可以理解,原本的翻译「如果 T 满足 U,就回传 T,否则回传 never」并没有错,只是要加上分配律的概念。

所以:

type T1 = Extract<'a' | 'b' | 'c', 'a'>; //  'a'

等同於:

type T1 = Extract<'a', 'a'> | Extract<'b', 'a'> | Extract<'c', 'a'>;

会变成:

type T1 = 'a' | never | never; // 'a'

never 就是个空集合的概念,任何东西和它取交集,还是原本的东西,因此最後就得到的 type T1 = 'a',是不是不会太难理解呢?

Exclude 的原始码

接着让我们来看 Exclude 的原始码:

Exclude

你会发现它和 Extract 最大的差别就是,ExcludeT 满足 U 是会回传 never,而 Extract 则是会回传 T

回到范例,现在读者应该也可以理解:

type T4 = Exclude<'a' | 'b' | 'c', 'a' | 'f'>; // 'b' | 'c

等同於:

type T4 =
  | Exclude<'a', 'a' | 'f'>
  | Exclude<'b', 'a' | 'f'>
  | Exclude<'c', 'a' | 'f'>;

会变成:

type T4 = never | 'b' | 'c';

最终就会得到 'b' | 'c' 的结果。

NonNullable

在 TypeScript 内建的 Utility Types 中还有个 NonNullable,它可以把型别中可能存在的 nullundefined 都过滤掉,关於它的用法可以直接参考官网上的说明,而它的 source code 是长这样:

// Exclude null and undefined from T
type NonNullable<T> = T extends null | undefined ? never : T;

如果读者对於上面 ExtractExclude 已经有足够的理解,相信一定也能够理解 NonNullable 的原始码是如何作用以及达到预期的效果的,试着理解看看吧!

不要使用分配律

预设的情况 Conditional Types 都会使用分配律,但如果有某些使用读者在写自己的 Utility Type 不希望使用分配律是,可以使用在 extends 前後的型别加上中括号 [] 来达成。例如,我们改写原本的 Extract 让它没有分配律,也就是改成 [T][U],像是这样:

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

这时候,如果我们一样带入 union type,这个 Utility Type 会是完全不同的意义:

type NoDistributeExtractReturn1 = NoDistributeExtract<
  'a' | 'b',
  'a' | 'b' | 'c'
>; // 'a' | 'b'

没有分配律的使用下会直接拿 'a' | 'b'(T)和 'a' | 'b' | 'c'(U)来比较,这里因为 T 满足 U 所以会直接回传 T

同样的,如果 T 不满足 U 的话:

type NoDistributeExtractReturn2 = NoDistributeExtract<'a' | 'b', 'a' | 'c'>; // never

因为 'a' | 'b'(T) 不满足 'a' | 'c'(U),则会回传 never

范例程序码

https://tsplay.dev/wO8YyN @ TypeScript Playground

参考资料


<<:  Day10 Vue模板语法 & V-text、V-html、V-once介绍

>>:  【Day 09】if ... else

Backtrader - 自订 datafeeds

我们之前在喂历史资料,都是先用 shioaji 下载下来,然後再用 padas 转成 datafra...

用e-paper做普普风格影像显示

这次使用的元件是1.54inch_e-paper_b (黑白红显示) Pin Layout VCC ...

Day 11 - 丰收款非官方 PHP SDK 发布

因为要陪老婆追剧鱿鱼游戏,所以还有几个测试还没写完,但大致上这个 PHP SDK 的 API 已经开...

铁人赛失败了....

可能是我半夜发文的关系 所以超过十二点了 铁人赛给我失败了 不过也没关系拉我继续记录 今天继续看fl...

又是一个无止尽的夜,公司网站与资料库同时坏掉。

这是种想哭也哭不出来的心情。 总经理用着严肃的口气骂着资讯人员,为什麽系统会毁损。 状况出现 在科技...