TypeScript 我们能否减少Object.assign的重载?

j2qf4p5b  于 6个月前  发布在  TypeScript
关注(0)|答案(5)|浏览(69)

We can convert unions into intersections (感谢 @jcalz!),我们可以很好地对可变参数进行建模,所以我想尝试用一个重载来建模 Object.assign 。以下是我得出的结果:

// Maps elements of a tuple to contravariant inference sites.
type MapContravariant<T> = {
    [K in keyof T]: (x: T[K]) => void
}

type TupletoIntersection<T, Temp = MapContravariant<T>> =
    // Ensure we can index with a number.
    Temp extends Record<number, unknown>
        // Infer from every element now.
        ? Temp[number] extends (x: infer U) => unknown ? U : never
        : never;

declare function assign<T extends object, Rest extends object[]>(
    x: T,
    ...xs: Rest
): T & TupletoIntersection<Rest>;

不幸的是,这在以下情况下给出的结果并不完全正确:

let asdf = assign({x: "hello"}, Math.random() ? {x: "hello"} : {z: true });

目前,asdf 的类型是:

| ({ x: string; } & { x: string; z?: undefined; })
| ({ x: string; } & { z: boolean; x?: undefined; })

多么美丽的类型啊!不幸的是,你可以看到联合体的第二个元素试图将具有冲突属性的类型相交,对于 x 这是没有意义的。
此外,@weswigham 指出,这对于最初只是一个普通数组的情况并不适用,该数组的元素最终被扩展到 Object.assign 中。
我不禁想知道我们是否能在这里做得更好。

dtcbnfnu

dtcbnfnu1#

asdf 的疯狂类型与你目前从 Object.assign() 获得的相同,对吗?如果目标是替换各种数量的硬编码交集的重载,我认为像上面的那样可以实现。
还是目标是用比包交集更适用的返回类型替换 Object.assign() 的返回类型(例如 Spread<L, R> 的一些泛化)?

gmxoilav

gmxoilav2#

在几个TS版本的顶峰之后,我认为我找到了一种在我能想到的情况下导致正确答案的打字方式:

type ExtractNonNever<T> = {[K in keyof T as undefined extends T[K] ? K : never]?: T[K]}
type OptionalKeyOf<T> = keyof ExtractNonNever<{[K in keyof T]: never}>
type RequiredKeyOf<T> = Exclude<keyof T, OptionalKeyOf<T>>

type AssignProperties<T, U> = {
    [K in keyof T | keyof U]:
        K extends RequiredKeyOf<U> ? U[K] :
            | (K extends keyof T ? T[K] : never)
            | (K extends OptionalKeyOf<U> ? U[K] : never)
}

type DistributiveAssignProperties<T, U> =
    T extends infer S
        ? U extends infer V
            ? AssignProperties<S, V> : never : never;

type AssignAllProperties<T, U extends any[]> = 
    U extends [infer Next, ...infer Rest]
        ? AssignAllProperties<DistributiveAssignProperties<T, Next>, Rest>
        : T

declare function assign<T extends object, Rest extends object[]>(
    x: T,
    ...xs: Rest
): AssignAllProperties<T, Rest>;

类型

let asdf = assign({x: "hello"}, Math.random() ? {x: "hello"} : {z: true});

被正确地推断为 { x: string, z?: undefined } | { x: string | undefined, z: boolean }
Playground链接,它也有类型测试: https://tsplay.dev/4w1LyW
我确实想知道我们是否能在这里做些什么更好。
@DanielRosenwasser,这是否算作某种“更好”的定义?

xsuvu9jc

xsuvu9jc3#

@lazytype 真有趣,我们竟然同时想到了这个!这是我对可变参数Object.assign类型的实现,它简单地创建了传入的所有类型之间的交集——就像所有重载函数手动所做的那样。

// Helper type - copied from typescript internals (https://github.com/microsoft/TypeScript/blob/6714998/src/compiler/types.ts#L8100)
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
  k: infer I
) => void
  ? I
  : never;

// Helper type
type ArrayElementType<Arr> = Arr extends (infer ElementType)[]
  ? ElementType
  : never;

// ✅ New assign type
function assign<T extends object, S extends any[]>(
  target: T, ...sources: S
): T & UnionToIntersection<ArrayElementType<S>>

这里是查看它的行动方式的链接! playground link
请注意,这个例子的简单性在某种程度上导致它在底部的Math.random示例中出现问题,以至于我现在还不太理解。
当然很乐意看到这样的东西被添加进来!我遇到了这个问题,即非人为的方式多次将可变参数Object.assign调用类型化为any,所以说这已经困扰我一段时间了。

ukqbszuj

ukqbszuj4#

请注意,此示例的简单性在某种程度上导致它在底部的 Math.random 示例中出现问题,以至于我现在无法100%理解。

@tvler 由于 Math.random() ? {x: "hello"} : {z: true} 的类型为 { x: string; z?: undefined } | { z: boolean; x?: undefined },当它尝试应用 UnionToIntersection 时,会得到 { x: string; z?: undefined } & { z: boolean; x?: undefined },而 { x: string; z?: undefined } & { z: boolean; x?: undefined } 只能是 never

基本上,当 assign 的参数具有重叠属性时,联合到交集的方法总是会出现这种问题。要处理这种情况,需要让参数列表中后面的属性替换前面的属性,而不是与它们相交。我在提案中定义的辅助类型 AssignProperties 试图做到这一点。

iyr7buue

iyr7buue5#

好的,谢谢你的解释!就像我说的,我直到自己发现这个问题之前都没有看到你的工作,很高兴看到其他人对此有更多的思考。

相关问题