2

Given the below


type Constructor<T> = new (...args: any[]) => T;

type MakeFirstUndefined<T extends unknown[]> = {
  [I in keyof T]: I extends "0" ? undefined : T[I]
}

type FilterUndefined<T extends unknown[]> = T extends [] ? [] :
    T extends [infer H, ...infer R] ?
    H extends undefined ? FilterUndefined<R> : [H, ...FilterUndefined<R>] : T

When I apply it to a tuple, like below, I'm able to filter out the first element

type InputTuple = [number, string, object] 

type FirstUndefinedTuple = MakeFirstUndefined<InputTuple> // Type [undefined, string, object]
const outputFirstUndefinedSuccess: FirstUndefinedTuple = [undefined, 'a-string', {}]
const outputFirstUndefinedCorrectlyErrors: FirstUndefinedTuple = [1, 'a-string', {}]

type FirstItemFilteredOut = FilterUndefined<FirstUndefinedTuple> // Type [string, object]
const ouputFilteredSuccess: FilterUndefined<FirstUndefinedTuple> = ['a-string', {}]
const ouputFilteredCorrectlyErrors: FilterUndefined<FirstUndefinedTuple> =  ['a-string', {}, 'apple']

When I then apply the same concept to the ConstructorParameters of a specific Constructor. It works.

class MagicMonkey {
  constructor(public name: string, public tricks: string[]) {
    
  }
}

enum Color {
  BLUE,
  GREEN,
  PURPLE,
  RED,
  PINK,
}

class FlashyFlamingo {
  constructor(public name: string, public colors: Color[], wingSpan: number) {
    
  }
}

function createMagicMonkey(...args: FilterUndefined<MakeFirstUndefined<ConstructorParameters<typeof MagicMonkey>>>) {
  return new MagicMonkey('jack', ...args);
}

console.log(createMagicMonkey(['trick 1', 'trick 2']))

console.log(createMagicMonkey(['trick 1', 'trick 2'], 1000)) // Correct Error: Expected 1 arguments, but got 2

// Output:
// MagicMonkey: {
//   "name": "jack",
//   "tricks": [
//     "trick 1",
//     "trick 2"
//   ]
// } 

function createFlashyFlamingo(...args: FilterUndefined<MakeFirstUndefined<ConstructorParameters<typeof FlashyFlamingo>>>) {
  return new FlashyFlamingo('', ...args);
}

console.log(createFlashyFlamingo([Color.PINK, Color.PURPLE], 1000))
console.log(createFlashyFlamingo([Color.PINK, Color.PURPLE])) // Correct Error: Expected 2 aguments, but got one

// Output:
// FlashyFlamingo: {
//   "name": "",
//   "colors": [
//     4,
//     2
//   ]
// } 

However if I attempt to generify it to any Constructor. It does not work.


function createGeneric<T extends Constructor<any>>(ctor: T, ...args: FilterUndefined<MakeFirstUndefined<ConstructorParameters<T>>>) {
   return new ctor('jack', ...args);
}

console.log(createGeneric(MagicMonkey, ['trick 1', 'trick 2']))

// Expected Ouput(same as createMagicMonkey): 
// MagicMonkey: {
//   "name": "jack",
//   "tricks": [
//     "trick 1",
//     "trick 2"
//   ]
// } 

console.log(createGeneric(FlashyFlamingo, [Color.PINK, Color.PURPLE], 1000))

// Expected Ouput(same as createFlashyFlamingo): 
// FlashyFlamingo: {
//   "name": "",
//   "colors": [
//     4,
//     2
//   ]
// } 


I get the error: A rest "parameter must be of an array type"

As a test I removed the FilterUndefined part and even on just applying MakeFirstUndefined it still says "parameter must be of an array type". It's definitely a typle after applying MakeFirstUndefined and then MakeFirstUndefined as the example demonstrates.

I don't see how it could be anything else other than a tuple, seeing that ConstructorParamters returns a tuple. Mapped tuple types are a thing now, right?

Playground

EDIT: 2021/06/24 UTC 9:35 Update playground with usage examples and added FilterUndefined to the createGeneric function.

EDIT: 2021/06/24 UTC 16:08 Update playground to make it clear that the goal is to remove the first parameter type from any constructor.

1 Answers1

0

I have replaced MakeFirstUndefined with new one. It is hard to figure out for TS that

type MakeFirstUndefined<T extends unknown[]> = {
  [I in keyof T]: I extends "0" ? undefined : T[I]
}

produces an array.

type TupleWithUndefined = [number, string, undefined, number];

// we need to infer constructor parameters
type Constructor<Args extends string[]> = new <R>(...args: [...Args]) => R;

type MakeFirstUndefined<T extends unknown[]> = T extends [infer _, ...infer Rest] ? [undefined, ...Rest] : T

type FilterUndefined<T extends unknown[]> =
    T extends [] ? [] :
    T extends [infer H, ...infer R] ?
    H extends undefined ? FilterUndefined<R> : [H, ...FilterUndefined<R>] : T


type InputTuple = [number, string, object]
type FilteredTuple = FilterUndefined<MakeFirstUndefined<InputTuple>> // [string, tuple] 

const error: FilteredTuple = [1, '2', {}]
const successful: FilteredTuple = ['2', {}]


class MagicMonkey {
    constructor(name: string, public tricks: string[]) {

    }
}

function create(...args: FilterUndefined<MakeFirstUndefined<ConstructorParameters<typeof MagicMonkey>>>) {
    return new MagicMonkey('jack', ...args);
}

function createGeneric<El extends string, Tuple extends El[]>(...args: MakeFirstUndefined<ConstructorParameters<Constructor<Tuple>>> & string[]) {
    return new MagicMonkey('jack', args);
}

Please, let me know if it works for you

Playground

MagicMonkey expect second argument to be string[] but I did not find any stirng[] constraints in your code. Maybe it worth adding?

UPDATE


type Constructor<Args extends string[]> = new <R>(...args: [...Args]) => R;

class MagicMonkey<Trick extends string, Tricks extends Trick[]> {
    constructor(public name: string, public tricks: [...Tricks]) {

    }
}

function createGeneric<El extends string, Tuple extends El[]>(...args: ConstructorParameters<Constructor<Tuple>>) {
    return new MagicMonkey('jack', args);
}

const result2 = createGeneric('trick1') // MagicMonkey<string, ["trick1"]>