1
assign<T extends {}, U>(target: T, source: U): T & U;
// ...
assign<T extends {}, U, V, W>(target: T, source1: U, source2: V, source3: W): T & U & V & W;
assign(target: object, ...sources: any[]): any;

This is the type in lib. An obvious problem is that as long as more than 4 parameters are passed in, it will return an any type, but it will lose the meaning of using TypeScript. Is there a better solution?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
flycran
  • 13
  • 2

1 Answers1

0

This is the subject of microsoft/TypeScript#28323, a longstanding suggestion to use conditional type inference to convert the tuple of source inputs to an intersection programmatically instead of using a long list of overloads. See Transform union type to intersection type for the basic technique.

We can write a TupleToIntersection<T> utility type which takes a tuple type T, maps it to a version with the tuple elements in contravariant position (see Difference between Variance, Covariance, Contravariance and Bivariance in TypeScript for more information), and infers a single element type from there, which will be an intersection because of the rules for infer:

type TupleToIntersection<T extends any[]> =
    { [I in keyof T]: (x: T[I]) => void }[number] extends
    (x: infer I) => void ? I : never;

Armed with this, we can merge an appropriate method signature into the existing global ObjectConstructor interface (which should be wrapped in a declare global block if you do so inside a module):

// declare global {
interface ObjectConstructor {
    assign<T extends {}, U extends any[]>(
        target: T, ...source: U
    ): T & TupleToIntersection<U>;
}
// }

Let's test it out:

const foo = Object.assign(
    { a: 1 }, { b: 2 }, { c: 3 }, { d: true }, { e: "five" },
    { f: new Date() }, { g: null }, { h: undefined }, { i: 100n },
    { j: /abc/ }
);

/* const foo: { a: number; } & { b: number; } & { c: number; } & 
  { d: boolean; } & { e: string; } & { f: Date; } & 
  { g: null; } & { h: undefined; } & { i: bigint; } & 
  { j: RegExp; } */

Looks good. The output type is indeed the intersection of all the input types, as desired.

(Note that this question doesn't seem to be asking about the issue where intersections are only an approximation of what Object.assign() does, and that things go wrong when properties get overwritten. For example, if you call Object.assign({a: 1}, {a: "two"}), the actual output will be {a: "two"} of type {a: string}, but TypeScript gives the inaccurate/unhelpful type {a: number} & {a: string} which means that it thinks the a property is of the impossible never type. This drawback is present in both the original TS library and in the tuple-to-intersection version in this answer. Because the question isn't asking about that, this answer is not going to do anything with it. If you're interested in tackling that, you should look at Typescript, merge object types? instead.)

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360