Typescript: Map derived from discriminated union has wrong type when discriminator property has the same value

Created on 9 Apr 2020  路  2Comments  路  Source: microsoft/TypeScript

TypeScript Version: 3.8.3


Search Terms:

  • map from discriminated union same key
  • discriminated union same key

Expected behavior:

CatMap should have type

type CatMap = {
    cat: TerrestrialCat[] | AlienCat[];
}

Actual behavior:

CatMap has type

type CatMap = {
    cat: AlienCat[];
}


Related Issues:

Code

enum TerrestrialAnimalTypes {
    CAT = "cat",
};

enum AlienAnimalTypes {
    CAT = "cat",
};

type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes;

interface TerrestrialCat {
    type: TerrestrialAnimalTypes.CAT;
    address: string;
}

interface AlienCat {
    type: AlienAnimalTypes.CAT
    planet: string;
}

type Cats = TerrestrialCat | AlienCat;

// type A = TerrestrialCat | AlienCat
type A = Extract<Cats, { type: "cat" }>;

/*
 * type CatMap = {
 *   cat: AlienCat[];
 * }
 */
type CatMap = {
    [V in AnimalTypes]: Extract<Cats, { type: V }>[]
};

Output

"use strict";
var AnimalTypes;
(function (AnimalTypes) {
    AnimalTypes["CAT"] = "cat";
})(AnimalTypes || (AnimalTypes = {}));
;

Compiler Options

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "useDefineForClassFields": false,
    "alwaysStrict": true,
    "allowUnreachableCode": false,
    "allowUnusedLabels": false,
    "downlevelIteration": false,
    "noEmitHelpers": false,
    "noLib": false,
    "noStrictGenericChecks": false,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "esModuleInterop": true,
    "preserveConstEnums": false,
    "removeComments": false,
    "skipLibCheck": false,
    "checkJs": false,
    "allowJs": false,
    "declaration": true,
    "experimentalDecorators": false,
    "emitDecoratorMetadata": false,
    "target": "ES2017",
    "module": "ESNext"
  }
}

Playground Link: Provided

Bug Fix Available

Most helpful comment

I... guess this is a bug? When we resolve mapped type members from a union constraint type like we have here, we build up a symbol table of members, with string keys corresponding to the constituents of the union. Since both TerrestrialAnimalTypes.CAT and AlienAnimalTypes.CAT have the property name 'cat', we do create two separate member symbols, but then we simply (accidentally?) overwrite the first with the second in the table.

(The bit that happens in Extract is _not_ a bug; the assignability of identically-valued but separately-declared string enums is well-defined.)

It鈥檚 only a few lines of code to check for an existing property by the same name and fix up its nameType and mapper such that it produces a union type, and no existing tests break. I _think_ that鈥檚 reasonable and the result looks like desirable behavior.

Here鈥檚 a smaller example to consider:

enum TerrestrialAnimalTypes { CAT = "cat" };
enum AlienAnimalTypes { CAT = "cat" };

type T = { [K in TerrestrialAnimalTypes | AlienAnimalTypes]: K };

Today, T is { cat: AlienAnimalTypes }. In my branch, T is { cat: AlienAnimalTypes | TerrestrialAnimalTypes }. It feels slightly paradoxical that K could itself instantiate to a union, but I can鈥檛 see anything wrong with it.

All 2 comments

@andrewbranch I'm not 100% convinced this is a bug, but it's possible

I... guess this is a bug? When we resolve mapped type members from a union constraint type like we have here, we build up a symbol table of members, with string keys corresponding to the constituents of the union. Since both TerrestrialAnimalTypes.CAT and AlienAnimalTypes.CAT have the property name 'cat', we do create two separate member symbols, but then we simply (accidentally?) overwrite the first with the second in the table.

(The bit that happens in Extract is _not_ a bug; the assignability of identically-valued but separately-declared string enums is well-defined.)

It鈥檚 only a few lines of code to check for an existing property by the same name and fix up its nameType and mapper such that it produces a union type, and no existing tests break. I _think_ that鈥檚 reasonable and the result looks like desirable behavior.

Here鈥檚 a smaller example to consider:

enum TerrestrialAnimalTypes { CAT = "cat" };
enum AlienAnimalTypes { CAT = "cat" };

type T = { [K in TerrestrialAnimalTypes | AlienAnimalTypes]: K };

Today, T is { cat: AlienAnimalTypes }. In my branch, T is { cat: AlienAnimalTypes | TerrestrialAnimalTypes }. It feels slightly paradoxical that K could itself instantiate to a union, but I can鈥檛 see anything wrong with it.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siddjain picture siddjain  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments

manekinekko picture manekinekko  路  3Comments

bgrieder picture bgrieder  路  3Comments

Zlatkovsky picture Zlatkovsky  路  3Comments