Search code examples
typescriptunion-types

Conflicting types in some constituents


I'm trying to have a method return a specific type depending on a string given as an argument. This is what I have:

interface Base {
    type: 'a' | 'b';
}

interface MyA extends Base {
    type: 'a';
    nameA: string;
}

interface MyB extends Base {
    type: 'b';
    nameB: string;
}

interface Mappings {
    a: MyA;
    b: MyB;
}

const testMethod = <K extends keyof Mappings>(
    type: K
): Mappings[K][] => {
    const values: Mappings[K][] = [];

    if(type === 'a') {
        values.push({
            type: 'a',
            nameA: 'My name a'
        });
    }
    else if(type === 'b') {
        values.push({
            type: 'b',
            nameA: 'My name b'
        });
    }

    return values;
}

So if I run testMethod('a') it would have a return type of MyA[] and when I use testMethod('b') it would return MyB[]. But no matter what I do I can't get it to work. The errors are always:

Argument of type 'MyA' is not assignable to parameter of type 'Mappings[K]'. Type 'MyA' is not assignable to type 'never'. The intersection 'MyA & MyB' was reduced to 'never' because property 'type' has conflicting types in some constituents.

I thought the array could be the problem but returning a variable of type Mappings[K] has the same issue. Here is the playground link. I've done a similar thing in the past like this one to modify a function's argument according to the string given as argument, but here I'm completely lost.

Any guidance or typescript resources I can read would be appreciated!


Solution

  • Curiously, this worked for me as soon as I specified that Mappings extended Record<string, MyA | MyB>. I removed the common ancestor Base as it didn't seem to help.

    interface MyA {
        type: 'a';
        nameA: string;
    }
    
    interface MyB {
        type: 'b';
        nameB: string;
    }
    
    interface Mappings extends Record<string, MyA | MyB> {
        a: MyA;
        b: MyB;
    }
    
    // same as above
    
    const arrayOfMyA = testMethod('a');  // typed as MyA[]
    const arrayOfMyB = testMethod('b');  // typed as MyB[]
    

    I don't have an intuitive explanation, other than that an explicit union is the only way to convince TypeScript that 'a' | 'b' (keyof Mappings) exhaustively results in MyA | MyB...despite the fact that Mappings[keyof Mappings] correctly results in MyA | MyB both before and after the change! This might be a TypeScript bug, or at least an opportunity for an improvement.

    type MappingResults = Mappings[keyof Mappings];
    // typed as MyA | MyB, regardless of whether
    // Mappings extends Record<string, MyA | MyB>!
    

    Playground Link