TypeScript: Casting enums with same values gets error

TypeScript Version: 3.9.2

Search Terms:

Expected behavior: It would be great to check if all enum values are exclusively overlap, and only throw error when it is not.

Actual behavior: Now it shows error even when they have exactly same values:

Conversion of type 'EnumA' to type 'EnumB' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.(2352)

Related Issues:

Code

export enum EnumA {
  Key = 'VALUE',
}

export enum EnumB {
  Key = 'VALUE',
}

function assign(val: EnumA) {
  const val2: EnumB = val as EnumB;  // <--- getting error here
  return val2;
}

Output
export var EnumA;
(function (EnumA) {
    EnumA["Key"] = "VALUE";
})(EnumA || (EnumA = {}));
export var EnumB;
(function (EnumB) {
    EnumB["Key"] = "VALUE";
})(EnumB || (EnumB = {}));
function assign(val) {
    const val2 = val;
    return val2;
}

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

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 6
  • Comments: 15 (4 by maintainers)

Most upvoted comments

Hey, any updates on this?

This could happen often, if you are migrating to graphql, and then generate all types including enums separately in graphql schema.

Does TS’s structural typing philosophy not extend to enums?

this seems to work

type A = { name: string };
type B = { name: string };

const f = (x: A) => x.name;

const b: B = { name: 'george' };

f(b); // no type errors

Would it be natural for the enum equivalent to work too?

This could happen often, if you are migrating to graphql, and then generate all types including enums separately in graphql schema.

@zlk89 this is exactly what I’m experiencing right now. This is our setup

  • graphql schema in .graphql files generating schema-types.ts file
  • BE team gives us OpenAPI schema we generate types from

A user makes a request to our GraphQL service, and we pass it along to our REST service.

Here is a shortened version of the generated schema-types.ts (GraphQL input)

export type RegistryDetailInput = {
  /** Access type of the registry (PRIVATE, PUBLIC) */
  accessType: Access;
};
export enum Access {
  PRIVATE = 'PRIVATE',
  PUBLIC = 'PUBLIC',
}

And here are the OpenAPI generated types (BE)

export interface BaseRegistryV3Details {
    accessType: BaseRegistryV3Details.accessType;
}
export declare namespace BaseRegistryV3Details {
    enum accessType {
        PUBLIC = "PUBLIC",
        PRIVATE = "PRIVATE"
    }
}

Now trying to run

updateRegistry({accessType: gqlInput.accessType})

gives

Type '{ accessType: Access;}' is not assignable to type 'BaseRegistryV3Details'. Types of property 'accessType' are incompatible. Type 'Access' is not assignable to type 'accessType'.

Trying

updateRegistry({accessType: gqlInput.accessType as BaseRegistryV3Details.accessType}) // 🤢

gives

Conversion of type 'Access' to type 'accessType' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

And of course I could just do

updateRegistry({accessType: gqlInput.accessType as unknown as BaseRegistryV3Details.accessType}) // 🤮

but that’s pretty #cringe haha

A bit late to the party but maybe someone can help me figure this one out.

I love the fact that enums are treated like nominal types – to be honest I’d like to see more options for nominal types in TS but that’s for a whole different discussion.

Basically I’m dealing with the same situation as OP so let us assume, just for the sake of this thought experiment, there is no other way.

Is there a type-safe way for me to cast one enum to the other? I.e. using some combination of conditional and mapped types? I tried creating a conditional type that would return never if keys of the source didn’t extend keys of the target (through keyof typeof Enum) so that the unsafe cast through unknown would still fail if the enums didn’t overlap, like so:

type CastToTypeIfKeysMatch<From, To> = keyof From extends keyof To ? To : never;
const foo: EnumA = bar as unknown as CastToTypeIfKeysMatch<typeof bar, EnumA>

but then I realised I can not assign anything to never, but I can assign never to anything so that idea went down the drain.

Anyone got any other suggestions? 😅