[答案公布] TypeScript 每周挑战第 2 期:条件类型重组

·  阅读 553
[答案公布] TypeScript 每周挑战第 2 期:条件类型重组

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

其实大多数 TypeScript 开发者,对 TypeScript 的利用,还停留在初级水平。

不信?来试试 TypeScript 每周挑战吧!

第 2 期挑战的题目是:如何在 Pick<A|B> 后仍然保持 Union Type 中的字段关系。先来回顾一下题目吧~

题目回顾

对于类型定义:

type Data = {
    type: 'a',
    value: {
        a: string
    },
    a1: string,
    a2: string,
    // ...
} | {
    type: 'b',
    value: {
        b: string
    },
    b1: string,
    b2: string,
    // ...
} | {
    type: 'c',
    value: {
        c: string
    },
    c1: string,
    c2: string,
    // ...
}; // 可能还有更多项

type PickData = Pick<Data, 'type' | 'value'>;
复制代码

经过 TypeScript 的转换处理,PickData 变成了:(不期望)

type PickData = {
    type: "a" | "b" | "c";
    value: {
        a: string;
    } | {
        b: string;
    } | {
        c: string;
    };
}
复制代码

而实际 期望的 PickData 结果应该是:

type PickData = {
    type: 'a',
    value: {
        a: string
    }
} | {
    type: 'b',
    value: {
        b: string
    }
    // ...
} | {
    type: 'c',
    value: {
        c: string
    }
};
复制代码

请你重新定义 问题 中的 PickData,使其在 Picktypevalue 字段后仍能保持原始 Data 中的互斥匹配关系。

请尝试在代码量和类型冗余最小的情况下实现。

答案公布

type PickUnion<T, K extends keyof T> = T extends any ? Pick<T, K> : never;
type PickData = PickUnion<Data, 'type' | 'value'>;
复制代码

神奇吗,加上 T extends any ? XXX : never 的判断,竟然就可以了! 这是为什么呢?

原理解析

Pick<T, K> 实际上是 TypeScript 的 Mapped Type 特性:

Pick<T, U extends keyof T> = {
    [K in U]: T[K]
}
复制代码

Mapped Type 字面意思就是映射类型,映射就是从一个类型映射到另一个类型。 所以对于题目而言,Pick<Data, 'type'|'value'> 的映射结果就是:

{
    type: Data['type'],
    value: Data['value']
}
复制代码

可以看到,映射过后的类型,字段间并没有关联,所以 Union Type 中的关系也就不存在了。

因此,TypeScript 提供了一种叫做 条件类型重组 (Distributive Conditional Types)的机制来解决类似的问题。其写法就是:

type XXX<T> = T extends any ? AAA : BBB;
复制代码

如果泛型参数 T 是一个条件类型(例如 Union Type),则 TypeScript 在推导类型时会进行重组操作,举个例子,假设你给 T 传递的是 A | B

// 重组前
type XXX<A | B> = (A | B) extends any ? AAA : BBB;
// 重组后 ↓↓↓
type XXX<A | B> = (A extends any ? AAA : BBB) | 
    (B extends any ? AAA : BBB);
复制代码

所以当我们实现为 PickUnion 的时候:

type PickUnion<T, K extends keyof T> = T extends any ? Pick<T, K> : never;
复制代码

我们传入的 T: Data 由于是一个条件类型 A | B | C,所以它被重组成了:

(A extends any ? Pick<A, K> : never) |
(B extends any ? Pick<B, K> : never) |
(C extends any ? Pick<C, K> : never)
复制代码

如此,就保留了条件类型中的字段关系。

了解更多条件类型重组的信息,参见 TypeScript 官方手册文档: Distributive Conditional Types


往期挑战回顾


如果你对使用 TypeScript 进行全栈开发感兴趣,欢迎关注目前世界上唯一支持 TypeScript 复杂类型运行时自动检测和二进制序列化的TypeScript 开源 RPC 框架 —— TSRPC

GitHub:github.com/k8w/tsrpc
中文文档:tsrpc.cn
视频教程:www.bilibili.com/video/BV1hM…

分类:
前端
分类:
前端