Max Gordon
Max Gordon

Reputation: 5457

Provide a second generic type conditionally based on the first type

I'm trying to generate a class definition for mongoose where I would like to use the first generic to select the second generic. A simplified concept version would look something like this:

class StringClass {
    val: string;

    constructor(val: string) {
        this.val = val;
    }
}

class NumberClass {
    val: number;

    constructor(val: number) {
        this.val = val;
    }
}

class test<A extends string | number, B extends typeof StringClass | typeof NumberClass) {
    val: B;
    
    constructor(val: A, Class: B) {
        this.val = new Class(val);
    }
}

I would like to change the test to be defined as:

class test<A extends string | number, B extends (A instanceof string ? typeof StringClass : typeof NumberClass)) {
    val: B;
    
    constructor(val: A, Class: B) {
        this.val = new Class(val);
    }
}

This is not valid in Typescript 4.4 and thus I wonder, how do I make the second generic dependent on the first parameter?

Upvotes: 1

Views: 423

Answers (1)

Why this error appears ? Here you have an example:

class StringClass {
    val: string;

    constructor(val: string) {
        this.val = val;
    }
}

class NumberClass {
    val: number

    constructor(val: number) {
        this.val = val;
    }
}

type Check<A> = A extends string ? typeof StringClass : typeof NumberClass

class test<A extends string | number, B extends Check<A>> {
    val: B;
    constructor(val: A, Class: B) {
        // Type 'string' is not assignable to type 'never'
        this.val = new Class(val);
    }
}

const result = new test('2', StringClass)

In above case val infered to never, because

multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

Hence string & number === never

COnsider this example:


class StringClass {
    val: { string: string };

    constructor(val: { string: string }) {
        this.val = val;
    }
}

class NumberClass {
    val: { number: number }

    constructor(val: { number: number }) {
        this.val = val;
    }
}

type Check<A> = A extends string ? typeof StringClass : typeof NumberClass

class test<A extends string|number, B extends Check<A>> {
    val: B;
    constructor(val: A, Class: B) {
        // Type 'string' is not assignable to type '{ string: string; } & { number: number; }'
        this.val = new Class(val);
    }
}

const result = new test('2', StringClass)

val is { string: string; } & { number: number; }.

So, how to fix it?

You can get rid of generics and overload your constructor with appropriate restrictions



class StringClass {
    val: string;

    constructor(val: string) {
        this.val = val;
    }
}

class NumberClass {
    val: number

    constructor(val: number) {
        this.val = val;
    }
}

type Check<A> = A extends string ? typeof StringClass : typeof NumberClass

interface Overloading {
    new(val: string): any
    new(val: number): any
    new(val: string | number): any
}

class test {
    val: StringClass | NumberClass
    constructor(val: number, Class: typeof NumberClass)
    constructor(val: string, Class: typeof StringClass)
    constructor(val: never, Class: typeof StringClass & typeof NumberClass) {
        this.val = new Class(val)

    }
}

const _ = new test('2', StringClass) // ok
const __ = new test(2, StringClass) // expected error

Playground

In general, runtime values can't rely on generic conditions (see Check). It is not safe.

This is why here:



class StringClass {
    val: string;

    constructor(val: string) {
        this.val = val;
    }
}

class NumberClass {
    val: number

    constructor(val: number) {
        this.val = val;
    }
}

class test<A, B extends {
    0: typeof StringClass,
    1: typeof NumberClass,
    2: never
}[A extends string ? 0 : A extends number ? 1 : 2]> {
    val: B
    constructor(val: A, Class: B) {
        this.val = new Class(val) // error

    }
}

you have an error.

YOu can always use type assertion as never in this case:


class StringClass {
    val: string;

    constructor(val: string) {
        this.val = val;
    }
}

class NumberClass {
    val: number

    constructor(val: number) {
        this.val = val;
    }
}

class test<A, B extends {
    0: typeof StringClass,
    1: typeof NumberClass,
    2: never
}[A extends string ? 0 : A extends number ? 1 : 2]> {
    val: StringClass | NumberClass
    constructor(val: A, Class: B) {
        this.val = new Class(val as never) // type asserion

    }
}

const _ = new test('2', StringClass) // ok
const __ = new test(2, StringClass) // expected error

Is it safe to use type assertion here? I'm not sure. I think it is up to you. If this code is just for testing - use as never or never just like I did.

If this a production code - please use conditional statements

Upvotes: 1

Related Questions