microsoft / TypeScript

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

The results of the 'Mapped Types' distribution have changed. #44325

Open SoraKumo001 opened 3 years ago

SoraKumo001 commented 3 years ago

Bug Report

The conversion result of 'Mapped Types' has changed in 4.3.2 or later typescript.

💻 Code

type Base = { 100: { a: number }; 200: { b: string } }
type NewType<T = Base> = {
  [P in keyof T ]: {
    code: P
    body: T[P]
  }
} extends {
  [P in any]: infer R
}
  ? R
  : never

let test: NewType

🙁 Actual behavior

let test: {
    code: keyof Base;
    body: {
        a: number;
    } | {
        b: string;
    };
}

🙂 Expected behavior

Hoping to return to pre-4.3.0-beta behavior!

whzx5byb commented 3 years ago

Related: https://github.com/microsoft/TypeScript/issues/44143

SoraKumo001 commented 3 years ago

I was able to temporarily work around the problem by inserting 'extends' once! This code also works in 4.3.2 and previous versions.

type Base = { 100: { a: number }; 200: { b: string } }
type NewType<T = Base> = {
  [P in keyof T ]:(({
    code: P
    body: T[P]
  }) )
} extends { 
  //[P in keyof T ]: infer R  
  [P in keyof T ]: unknown extends unknown ? infer R : never 
} 
  ? R
  : never

let test: NewType
vain0x commented 3 years ago

Another workaround (tried on ts 4.4.0-dev.20210530):

type NewType<T> = {
  [P in keyof T]: {
    code: P
    body: T[P]
  }
}[keyof T]

type Base = { 100: { a: number }; 200: { b: string } }
declare const test: NewType<Base>
ahejlsberg commented 3 years ago

Looks like this was broken by #43649.

ahejlsberg commented 3 years ago

@weswigham I'm not sure about the changes in #43649. They break the example in this issue because, as we check that inferences for R are assignable to the constraint of R (which is computed by #43649), the constraint contains un-instantiated references to T and the assignability check therefore fails. When then pick the constraint instead, which subsequently is instantiated with the type passed for T. But that's too late and not the result we want. We could potentially consider adding logic to somehow instantiate the constraint before the assignability check, but that would further complicate the inference process in conditional types and I'm not even sure I agree it's the right thing to do. I think it is perfectly fine for the inferred R to not have a constraint--and, honestly, there are many similar situations in which we don't produce constraints (e.g. if the check type is just a regular object type).

My recommendation would be to back out #43649. That of course means #43357, the issue it fixes, needs some other solution. As you yourself point out, it never really should have worked, and I think the right solution is to explicitly intersect with keyof T in the KeysWithoutStringIndex<T> type, i.e.

type KeysWithoutStringIndex<T> =
    ({ [K in keyof T]: string extends K ? never : K } extends { [_ in keyof T]: infer U } ? U : never) & keyof T;
weswigham commented 3 years ago

I think our syntactic inference constraint rules are kiiiiiinda arbitrary to begin with (what type parameters we match up across what locations has kinda been chosen by request) - I "fixed" the issue by adding one because the change in behavior was technically a regression, and adding those syntactic constraints is sort-of free to do. The extra instantiation would probably be the best thing we could do to keep the old behavior going. We could back it out... But then we'd be reintroducing the old regression...