2

I want to declare type as array of pairs containing constructor function and argument that can be passed into this constructor. And the same question about rest function arguments. How can I do it?

For example in the following code I want lines with { type: A, config: "" } to be an error as A does not accept string as a constructor argument:

interface ISmth<T> {
  value: T;
}

interface ISmthConstructor<T> {
  new(value: T);
}

class A implements ISmth<number> {
  constructor(public value: number) {}
}

class B implements ISmth<string> {
  constructor(public value: string) {}
}

interface ISmthConfig<T> {
  type: ISmthConstructor<T>;
  config: T;
}; 

function f<T extends any[]>(...args: T) { // What should I use instead of `any`?
  return args;                            // It should array of ISmthConfig<U> with any `U`
}

var x = f(
  { type: A, config: 12 },
  { type: B, config: "" },
  { type: A, config: "" }, // I want this argument to be an error
)

var y: ISmthConfig<any>[] = [ // What should I use instead of `any`?
  { type: A, config: 12 },
  { type: B, config: "" },
  { type: A, config: "" }, // I want this line to be an error
];
Qwertiy
  • 19,681
  • 15
  • 61
  • 128
  • Possible duplicate of [Typescript array of different generic types](https://stackoverflow.com/questions/51815782/typescript-array-of-different-generic-types) – Matt McCutchen Sep 22 '18 at 20:54

1 Answers1

2

First: I will change your definition of ISmthConstructor<T> to the following:

interface ISmthConstructor<T> {
  new(value: T): ISmth<T>; // added return type
}

The version in your code returns an any type implicitly, which is fine I guess but doesn't seem to be your intent.


Today's build of TypeScript (3.1.0-dev.20180922) gives me hope that the right answer is to tell you to wait until TypeScript 3.1 comes out sometime this month (Sep 2018) and then do this:

declare function f<T extends any[]>(
  ...args: { [K in keyof T]: ISmthConfig<T[K]> }
): void;

// no error, T inferred as [number, string]
f(
  { type: A, config: 12 }, 
  { type: B, config: "" }
);

// error, T inferred as [number, string, string | number]
f(
  { type: A, config: 12 }, 
  { type: B, config: "" },
  { type: A, config: "" } // error on type: string is not assignable to number
);

This is taking advantage of the upcoming mapped tuples feature, as well as inference from mapped types. Basically you are saying that the argument at position K must be an ISmthConfig<T[K]>. If the compiler can infer a tuple/array type T that meets this criterion, then it succeeds. Otherwise it reports an error on the offending parameter.

Anyway this is really promising!


If you need something that works on TypeScript 3.0, the best you're going to do is to pick some maximum reasonable length of argument list and make a function that can accommodate that:

function goodEnough<A0, A1, A2, A3, A4, A5, A6>(
  ...args: [ISmthConfig<A0>?, ISmthConfig<A1>?, ISmthConfig<A2>?, ISmthConfig<A3>?,
    ISmthConfig<A4>?, ISmthConfig<A5>?, ISmthConfig<A6>?]
) {
  return args;
}
goodEnough({ type: A, config: 12 }); // okay
goodEnough({ type: B, config: "" }); // okay
goodEnough(
  { type: A, config: 12 },
  { type: B, config: "" },
  { type: A, config: "" } // error, string not assignable to number
); 

This has the disadvantage of having an arbitrary limit and being verbose, but at least it works.

If the above signature ends up giving you an unusable type for args with lots of optional tuple elements, or you need something that works in TypeScript 2.9 and below, then you can do a series of overloads, which is even more verbose:

function ugh<A0>(a0: ISmthConfig<A0>): [typeof a0];
function ugh<A0, A1>(
  a0: ISmthConfig<A0>, a1: ISmthConfig<A1>
): [typeof a0, typeof a1];
function ugh<A0, A1, A2>(
  a0: ISmthConfig<A0>, a1: ISmthConfig<A1>, a2: ISmthConfig<A2>
): [typeof a0, typeof a1, typeof a2];
function ugh(...args: ISmthConfig<any>[]): ISmthConfig<any>[] {
  return args;
}

ugh({ type: A, config: 12 }); // okay
ugh({ type: B, config: "" }); // okay
ugh(
  { type: A, config: 12 },
  { type: B, config: "" },
  { type: A, config: "" } // error, string not assignable to number
); 

That's functional, but, well, ugh.


To recap: TypeScript 3.1 will solve this nicely; wait for that (or use it now) if you can. Otherwise, there are fallbacks/workarounds of various layers of distaste.

Hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360