上面这个是今天会提到的内容,如果你已经可以轻松看懂,欢迎直接左转去看我同事的精彩文章 — 「From State Machine to XState」!
前几天笔者提到的 Utility Types 多半是在 TypeScript 官方文件中提到的说明,但其实在 TypeScript 中也内建了一些 Utility Types,使用者不需要额外定义这些 Utility Types 就可以直接使用,这些内建的 Utility Types 列在官方网站的 references 中,今天就来让看其中两个内建的 Utility Types,分别是 Exclude
和 Extract
。
即使我们还不了解 Extract
和 Exclude
是怎麽被写出来的,但可以直接使用它,就好像有时不了解某个功能是如何被实作出来的,还是可以直接呼叫它提供的方法或函式一样。
还记得我们前面提到了解 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
需要接受两个参数 Type
和 Union
,但它会做的是把 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
虽然一样需要提供两个参数 Type
和 ExcludedUnion
,但它会做的是把 Type
中满足 ExcludedUnion
的剔除。所以在:
'a' | 'b' | 'c'
中剔除 a
後,只会剩下 'b' | 'c'
'a' | 'b' | 'c'
中剔除 'a' | 'b'
後,只会剩下 'c'
在知道了它们各种的用法後让我们来看它们的实作。
让我们先来看 Extract
的实作:
这里用到了我们昨天提到的 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
这个 Utility Type 的实作:
现在应该可以理解,原本的翻译「如果 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
的原始码:
你会发现它和 Extract
最大的差别就是,Exclude
是 T
满足 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'
的结果。
在 TypeScript 内建的 Utility Types 中还有个 NonNullable
,它可以把型别中可能存在的 null
或 undefined
都过滤掉,关於它的用法可以直接参考官网上的说明,而它的 source code 是长这样:
// Exclude null and undefined from T
type NonNullable<T> = T extends null | undefined ? never : T;
如果读者对於上面 Extract
和 Exclude
已经有足够的理解,相信一定也能够理解 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介绍
我们之前在喂历史资料,都是先用 shioaji 下载下来,然後再用 padas 转成 datafra...
这次使用的元件是1.54inch_e-paper_b (黑白红显示) Pin Layout VCC ...
因为要陪老婆追剧鱿鱼游戏,所以还有几个测试还没写完,但大致上这个 PHP SDK 的 API 已经开...
可能是我半夜发文的关系 所以超过十二点了 铁人赛给我失败了 不过也没关系拉我继续记录 今天继续看fl...
这是种想哭也哭不出来的心情。 总经理用着严肃的口气骂着资讯人员,为什麽系统会毁损。 状况出现 在科技...