使用 `Extract` 将一个对象并集映射到另一个具有泛型的对象并集

Mapping a union of objects to another union of objects with a generic using `Extract`

假设我有 Multi,一个由对象联合组成的类型:

interface MultiA {
    type: 'multi-a',
}
interface MultiB {
    type: 'multi-b',
}
type Multi = MultiA | MultiB;

我还有另一种类型 Ext,由以不同方式扩展到第一个对象的对象联合组成:

interface ExtA extends MultiA {
    foo: string
}
interface ExtB extends MultiB {
    bar: string
}
type Ext = ExtA | ExtB;

我想将 Multi 类型的值映射到 Ext,同时添加 ExtAExtB 所需的值。 示例:

function mapTypes<K extends Multi['type']>(
    val: Extract<Multi, { type: K }>
): Extract<Ext, { type: K }> {
    if (val.type === 'multi-a') {
        return {
            ...val,
            foo: '',
        }
    } else {
        return {
            ...val,
            bar: '',
        }
    }
}

我希望 Extract<Multi, { type: K } 可以毫无问题地映射到 return 类型 Extract<Ext, { type: K }>,因为 K 是相同的。

但是,作为错误消息的一部分显示 (Typescript Playground Link):

Type 'Extract<MultiA, { type: K; }> & { foo: string; }' is not assignable to type 'Extract<ExtB, { type: K; }>

Typescript 尝试使用 MultiAExtB 进行测试,这意味着它同时使用 K = 'multi-a'K = 'multi-b' 进行测试。

我想 Extract 类型在这里是不够的,因为它没有得到足够早的解决? 这里有什么选择?

TypeScript 编译器无法执行验证值是否可分配给 conditional type that depends on an unspecified generic 类型参数所需的那种高阶分析。

类型 Extract<Ext, { type: K }>if/else 块中使用 the Extract<T, U> utility type, which is implemented as a conditional type. And since K is a generic type parameter, the compiler is unable to see that {...val, foo: ''} or {...val, bar: ''} is assignable to it. It cannot use control flow analysis 来缩小 K

microsoft/TypeScript#33912 有一个开放的功能请求,以提供一些类型安全和方便的方法来实现通用函数 returning 条件类型,但目前还不可能。


一种继续进行的方法是接受你知道编译器不知道的东西并使用 type assertion to tell it that the return values are of type Extract<Ext, {type: K}>. Equivalently you can use write your function is a single-call-signature overload 允许实现比调用签名更松散的类型:

// call signature
function mapTypes<K extends Multi['type']>(
    val: Extract<Multi, { type: K }>): Extract<Ext, { type: K }>;

// implementation
function mapTypes(val: { type: Multi['type'] }) {
    if (val.type === 'multi-a') {
        return {
            ...val,
            foo: '',
        }
    } else {
        return {
            ...val,
            bar: '',
        }
    }
}

所以这个版本从调用者的角度表现得像你想要的那样,并在实现中放弃了一点compiler-guaranteed类型安全。


您可以采取的另一种方法是放弃像这样的条件类型并重构到一个版本,其中生成的类型在结构上等同于您想要的类型,即使它们不是这样显示的:

function mapTypes<K extends Multi['type']>(val: Extract<Multi, { type: K }>) {

    const k: K = val.type;

    const o = {
        "multi-a": { foo: "" },
        "multi-b": { bar: "" }
    }[k];


    return {
        ...val, ...o
    }
}

通过使用 K 类型的键对具有 Multi['type'] 类型键的对象执行 属性 查找,编译器将其表示为通用 indexed access type, and by spreading both val and this indexed access type together, the compiler represents this as an intersection type。因此这个版本的 mapTypes() 的 return 类型是:

// Extract<Multi, { type: K; }> & { "multi-a": { foo: string; }; "multi-b": { bar: string; };}[K]

这不太好,但事实证明它等同于您想要的输出类型。实际上,以下行编译没有错误:

const extA: ExtA = mapTypes({ type: "multi-a" }); // okay
const extB: ExtB = mapTypes({ type: "multi-b" }); // okay

所以这个版本在函数内部和外部都是类型安全的,但是调用者看到一个不太理想的 return 类型。不幸的是,我们在这里无法两全其美......出于与以前相同的原因,编译器无法判断丑陋和漂亮的 return 类型是等价的 generically 未指定 K;当您将 K 指定为 "multi-a""multi-b".

时,它只会看到等效项

当然,类型安全是否比调用签名美观更重要取决于您!

Playground link to code