microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.63k stars 12.44k forks source link

Refinement requirements fails for interface fields #49614

Open pakoito opened 2 years ago

pakoito commented 2 years ago

Bug Report

🔎 Search Terms

Enum refinement, enum unification, enum constant,

🕗 Version & Regression Information

3.3 and up, as far back as I could test it

⏯ Playground Link

Playground link with relevant code

💻 Code

const enum ToRefine {
    One,
    Two
}

type Refine<R extends ToRefine> = R extends ToRefine.One ? number : R extends ToRefine.Two ? string : never;
type RefineAny = Refine<ToRefine>;

function refined(param: Refine<ToRefine.One>) {
    return param + 1;
}

function callRefined(value: RefineAny) {
   refined(value)
}

interface Container<R extends ToRefine> {
    option: Refine<R>
}
type ContainerAny = Container<ToRefine>;

function contained(param: Container<ToRefine.One>) {
    return param.option + 1;
}

function callContained(value: ContainerAny) {
   contained(value)
}

🙁 Actual behavior

callRefined fails to typecheck due to a type mismatch as expected

callContained passes the typechecker when it shouldn't

🙂 Expected behavior

Both callRefined and callContained should fail to typecheck

whzx5byb commented 2 years ago

I think it has nothing to do with enums. A shorter repo here:

type IsOne<T> = T extends 1 ? true : false;
type Wrapped<T> = { data: IsOne<T> };

type V1 = IsOne<number> extends IsOne<1> ? true : false;
//   ^?
//   type V1 = false, expected
type V2 = { data: IsOne<number> } extends { data: IsOne<1> } ? true : false;
//   ^?
//   type V2 = false, expected
type V3 = Wrapped<number> extends Wrapped<1> ? true : false;
//   ^?
//   type V3 = true, bug?

Playground

RyanCavanaugh commented 2 years ago

It looks like we're not measuring the variance of Container correctly due to R only appearing in conditional type test positions.

Two possible workarounds:

interface Container<out R extends ToRefine> {
    option: Refine<R>;
}

or

type Refine<R extends ToRefine> = { [ToRefine.One]: number; [ToRefine.Two]: string }[R];