microsoft / TypeScript

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

always use literal types is too breaking #10938

Closed zpdDG4gta8XKpMCd closed 8 years ago

zpdDG4gta8XKpMCd commented 8 years ago
const a = 'a';
const aa = [a]; // string[], why????!
kitsonk commented 8 years ago

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 commented 8 years ago

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.

zpdDG4gta8XKpMCd commented 8 years ago

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 commented 8 years ago

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

zpdDG4gta8XKpMCd commented 8 years ago

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 commented 8 years ago

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.

zpdDG4gta8XKpMCd commented 8 years ago

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[] */
zpdDG4gta8XKpMCd commented 8 years ago

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
    */
}
zpdDG4gta8XKpMCd commented 8 years ago

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
        */
    }
})()
zpdDG4gta8XKpMCd commented 8 years ago

let me know if you need more

edevine commented 8 years ago
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 commented 8 years ago

@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.

zpdDG4gta8XKpMCd commented 8 years ago

@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!