always use literal types is too breaking #10938

Closed
aleksey-bykov opened this Issue Sep 15, 2016 · 13 comments

Projects

None yet

6 participants

@aleksey-bykov
const a = 'a';
const aa = [a]; // string[], why????!
@kitsonk
Contributor
kitsonk commented Sep 15, 2016

Because who would ever want an 'a'[] and if you do for some bizzare reason aa: 'a'[] = [a];... I assume there has to be some logical limit to how far literal types leak into other types, otherwise the inference would quickly get in the way, instead of being a productivity use.

Sometimes, a developer is just going to have to be explicit, as TypeScript's mind reading skills are limited.

@ahejlsberg
Member

You get a string[] because array elements are mutable locations:

const a = 'a';
const aa = [a];  // string[] because array elements are mutable

You could argue that const should be interpreted as deep constant (and thus the elements should keep their literal types), but that would break a lot of perfectly valid code.

@aleksey-bykov
aleksey-bykov commented Sep 15, 2016 edited

it's a good example of a breaking change
it didn't ask for type annotations, now it does
which makes it at odds with the seaming goal of this change: to avoid unnecessary type annotations

@thorn0
thorn0 commented Sep 15, 2016

@aleksey-bykov Are you saying aa was inferred to be 'a'[] before? In what version?

@aleksey-bykov

i have 200+ errors in my project, let's address them one-by-one

this used to work prooflink

type A = 'A';
type B = 'B';
const a: 'A' = 'A';
type X = A | B;
interface Data<a> {
    kind: a;
}
function dataFrom<a>(kind: a) : Data<a> {
    return { kind };
}
function useData(data: Data<X>) : void {
}
const data = dataFrom(a);
useData(data); /* <--- used to work, is broken now:
[ts] Argument of type 'Data<string>' is not assignable to parameter of type 'Data<X>'.
  Type 'string' is not assignable to type 'X'.
const data: Data<string>
*/
@mhegazy
Contributor
mhegazy commented Sep 15, 2016 edited

one workaround is adding a constraint on the type annotation in dataFrom, e.g.:

function dataFrom<a extends string>(kind: a) : Data<a> {

this will force inferring literal types if one exist. obviously it might not be what you intend if this is used for non-string types.

@aleksey-bykov
aleksey-bykov commented Sep 15, 2016 edited

let's move on, the following used to work

type A = 'A';
type B = 'B';
type X = A | B;
const a: A = 'A';
const kinds = [a];
function takeKinds(kinds: X[]) { }
takeKinds(kinds); /* <-- used to work, is broken now: 
[ts] Argument of type 'string[]' is not assignable to parameter of type 'X[]'.
  Type 'string' is not assignable to type 'X'.
const kinds: string[] */
@aleksey-bykov

keep going, the following used to work

type A = 'A';
type B = 'B';
type X = A | B;

export function isX(kind: X | string): kind is X {
    return true;
}

type Optional<a> = Some<a> | None;
type Some<a> = { some: a; };
type None = { none: void };
const none : None = { none: undefined };
function someFrom<a>(value: a): Some<a> { return { some: value}; }

export function asX(kind: X | string) : Optional<X> {
    return isX(kind) ? someFrom(kind) : none; /* <-- used to work, not anyore:
[ts] Type 'None | Some<string>' is not assignable to type 'Optional<X>'.
  Type 'Some<string>' is not assignable to type 'Optional<X>'.
    Type 'Some<string>' is not assignable to type 'Some<X>'.
      Types of property 'some' are incompatible.
        Type 'string' is not assignable to type 'X'.
(parameter) kind: X
    */
}
@aleksey-bykov
aleksey-bykov commented Sep 15, 2016 edited

the following used to work (i swear)

type A = 'A';
const a : A = 'A';
class X { public kind = a }
type B = 'B';
const b : B = 'B';
class Y  { public kind = b; }
type Z = X | Y;
declare const z: Z;
declare function doThis(x: X): void;
declare function doThat(y: Y): void;
declare function never(never: never): never { throw new Error(); }\
(function () {
    switch (z.kind) {
        case 'A': return doThis(z);
        case 'B': return doThat(z);
        default: return never(z); /* <-- used to work, not anymore
[ts] Argument of type 'Z' is not assignable to parameter of type 'never'.
  Type 'X' is not assignable to type 'never'.
const z: Z
        */
    }
})()
@aleksey-bykov

let me know if you need more

@aleksey-bykov aleksey-bykov changed the title from always use literal types is too braking to always use literal types is too breaking Sep 15, 2016
@edevine
edevine commented Sep 15, 2016 edited
switch (z.kind) {
    case 'A': doThis(z);
    case 'B': doThat(z);
    default: never(z); /* <-- used to work, not anymore
[ts] Argument of type 'Z' is not assignable to parameter of type 'never'.
  Type 'X' is not assignable to type 'never'.
const z: Z
        */
}

This is an extremely helpful pattern to ensure all cases are handled.

@ahejlsberg
Member

@aleksey-bykov The core issue with the breaks you're seeing is the assumption that a const with a type annotation is never widened. This sort of worked out previously because all other literals were eagerly widened to their base primitive type unless they occurred in a "literal context".

const a1 = 'A';  // Used to be type string, now is type 'A'
const a2: 'A' = 'A';  // Type 'A'

We've gotten lots of complaints (including from you I think) about the unintuitive difference between the declarations above because of the eager widening of literal types. With #10676 we work harder to preserve literal types, but that of course means expressions have literal types much more often. For that reason we now need to widen literal types when they're inferred for mutable locations (such as let variables, object literal properties, and array literal elements). If we didn't, we'd just have the opposite problem: You'd need an explicit type annotation on a let variable if it was initialized with a literal (e.g. let x: number = 1) to prevent us from giving it a literal type and defeating the purpose of it being mutable.

Now, because literal types were so rare before, we didn't widen types inferred for mutable locations. Instead, literal types would be inferred even for mutable locations. For example, given the a2 declared above, let x = a2 would infer the literal type 'A' for x which was sort of pointless. We did in fact get complaints about that too.

You could say that with #10676 it has gone from being a game of forcing constants to be given literal types to being a game of forcing mutable locations to keep literal types. I think that is a more meaningful way to look at it, but it is indeed a breaking change--specifically for code that exploited the "oddities" of the old design.

With respect to your examples, the first can be fixed by adding a string constraint to dataFrom:

function dataFrom<a extends string>(kind: a) : Data<a> { ... }

The second can be fixed by adding a type annotation to kinds:

const kinds: A[] = [a];

Third can be fixed by adding a constraint to someFrom:

function someFrom<a extends string>(value: a): Some<a> { ... }

Finally, the fourth can be fixed by adding a readonly modifier to the kind properties in class X and Y.

Also, in all of the examples you can delete the type annotations on const declarations with literal values because they're no longer necessary.

@aleksey-bykov

@ahejlsberg i am all right with fixing it by hands, just wanted to make sure that it now works as it is supposed to, after you said so no questions left, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment