Closed zpdDG4gta8XKpMCd closed 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.
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.
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
@aleksey-bykov Are you saying aa
was inferred to be 'a'[]
before? In what version?
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>
*/
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.
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[] */
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
*/
}
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
*/
}
})()
let me know if you need more
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.
@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.
@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!