Skip to content

type inference lost literal type #27704

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
hronro opened this issue Oct 11, 2018 · 11 comments
Closed

type inference lost literal type #27704

hronro opened this issue Oct 11, 2018 · 11 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@hronro
Copy link

hronro commented Oct 11, 2018

TypeScript Version: 3.1.2

Let assume we have a enum called Type. It seems type inference will treat const a = Type.A as const a: Type rather than const a: Type.A. And this may causes some problems.

Code

enum Type {
    A,
    B,
}

interface BaseType {
    type: Type;
    name: string;
}

interface T1 extends BaseType {
    type: Type.A;
    foo: number;
}

interface T2 extends BaseType {
    type: Type.B;
    bar: string;
}

type T = T1 | T2;

const a1: T = {
    type: Type.A,
    name: 'hello',
    foo: 100,
};
const a2 = {
    type: Type.A,
    name: 'hello',
    foo: 100,
};

const arr: T[] = [];

arr.push(a1);
arr.push(a2);

Expected behavior:
won't get any compile error in line arr.push(a2);.

Actual behavior:
get a type error in line arr.push(a2);.

Playground Link:
https://www.typescriptlang.org/play/index.html#src=enum%20Type%20%7B%0D%0A%20%20%20%20A%2C%0D%0A%20%20%20%20B%2C%0D%0A%7D%0D%0A%0D%0Ainterface%20BaseType%20%7B%0D%0A%20%20%20%20type%3A%20Type%3B%0D%0A%20%20%20%20name%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Ainterface%20T1%20extends%20BaseType%20%7B%0D%0A%20%20%20%20type%3A%20Type.A%3B%0D%0A%20%20%20%20foo%3A%20number%3B%0D%0A%7D%0D%0A%0D%0Ainterface%20T2%20extends%20BaseType%20%7B%0D%0A%20%20%20%20type%3A%20Type.B%3B%0D%0A%20%20%20%20bar%3A%20string%3B%0D%0A%7D%0D%0A%0D%0Atype%20T%20%3D%20T1%20%7C%20T2%3B%0D%0A%0D%0Aconst%20a1%3A%20T%20%3D%20%7B%0D%0A%20%20%20%20type%3A%20Type.A%2C%0D%0A%20%20%20%20name%3A%20'hello'%2C%0D%0A%20%20%20%20foo%3A%20100%2C%0D%0A%7D%3B%0D%0Aconst%20a2%20%3D%20%7B%0D%0A%20%20%20%20type%3A%20Type.A%2C%0D%0A%20%20%20%20name%3A%20'hello'%2C%0D%0A%20%20%20%20foo%3A%20100%2C%0D%0A%7D%3B%0D%0A%0D%0Aconst%20arr%3A%20T%5B%5D%20%3D%20%5B%5D%3B%0D%0A%0D%0Aarr.push(a1)%3B%0D%0Aarr.push(a2)%3B%0D%0A

Related Issues:

@ghost
Copy link

ghost commented Oct 11, 2018

When typing an object literal we don't know what type it's supposed to be without a hint. In a1 you provide that hint, but in a2 we have no idea and generate a new type. Since this contains literal types, they are widened by default., but T requires non-widened types so const a3: T = a2; won't work. To get this to work you need a type annotation on a2.

@ghost ghost added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Oct 11, 2018
@hronro
Copy link
Author

hronro commented Oct 23, 2018

@Andy-MS

I'm not familiar with PL, just a suggestion without confirming feasibility:

Can we make TypeScript Compiler infer literal types by default, and allow compiler re-infer the type of a variable while detecting variables are assigned new values?

For example:

let a = 'hello';    // now infer the type of `a` is `'hello'`;
a = 'world';        // because of the type of `a` is inferred, and type `'hello'` and type `'world'` are both literal type of type `string`, so now we can re-infer the type of `a` is `'hello' | 'world'`
a = 1;              // now assign a value which type is `1`, which is a literal type of `number` not `string`, so we may get an error here.
a = b as string;    // assume we don't have a wrong line above, now compiler should re-infer the type of `a` is `string`

And for those types are not inferred, we don't allow them can be re-inferred.

let a: 'hello' = 'hello';
a = 'world';        // got an error here. because the type of `a` is not inferred, so compiler won't re-infer here.

@ghost
Copy link

ghost commented Oct 23, 2018

@foisonocean That actually is possible if the variable has a string union type. But not if it has a string type, because we only narrow union types, and string isn't a union type.

function f(_: "foo") {}
function g(_: "bar") {}

let s: "foo" | "bar" = "foo";
f(s);
s = "bar";
g(s);

The problem is that without the explicit type annotation, the compiler can't time-travel and determine that it should have been "foo" | "bar" -- by the time it gets to the assignment, all it could do (if it had inferred s: "foo") is error. TypeScript's flow analysis can't expand the type of an existing variable, it can only narrow union types. To avoid that we just infer string for mutable variables instead of string literal types. This is a limitation that flow doesn't have because it can analyze all uses of s together.

@sarod
Copy link
Contributor

sarod commented Dec 28, 2018

I have a similar issue where the enum literal type is lost when going through a function with generic.

export enum Status {
    SAVING = 'SAVING',
    ERROR = 'ERROR'
}
export type State = SavingState | {
    _tag: Status.ERROR;
    prop2: any;
};
export type SavingState = {
    _tag: Status.SAVING;
    prop1: any;
};

const thisWorks: SavingState = {
    _tag: Status.SAVING,
    prop1: 'whatever'
};

const passThrough = <T>(arg: T): T => arg;

// The following line fails with
// error TS2322: Type '{ _tag: Status; prop1: string; }' is not assignable to type 'SavingState'.
// Types of property '_tag' are incompatible.
//   Type 'Status' is not assignable to type 'Status.SAVING'
const thisFailsWhenUsingEnum: SavingState = passThrough({
    _tag: Status.SAVING,
    prop1: 'whatever'
});

I found 2 possible workarounds in such situations:

// Workaround 1 specify the type in the generic function
const workaround1 = passThrough<SavingState>({
    _tag: Status.SAVING,
    prop1: 'whatever'
});

// Workaround 2 re-specify the type of Status.SAVING
const workaround2 = passThrough({
    _tag: Status.SAVING as Status.SAVING,
    prop1: 'whatever'
});

@sarod
Copy link
Contributor

sarod commented Dec 28, 2018

However it feels non-intuitive that the compiler uses string as the type for enum values instead of using the corresponding string literal.

I would expect the enum to behave similarly to the same code written using string literals:

// Using literal constants the same code starts working
export const Status = {
    SAVING: 'SAVING',
    ERROR: 'ERROR'
}
export type State = SavingState | {
    _tag: typeof Status.ERROR;
    prop2: any;
};

export type SavingState = {
    _tag: typeof Status.SAVING;
    prop1: any;
};

const passThrough = <T>(arg: T): T => arg;

// This fails using enum but works fine here
const thisWorks: SavingState = passThrough({
    _tag: Status.SAVING,
    prop1: 'whatever'
});

@LuminescentMoon
Copy link

Is this related?

interface Options {
 a: true
}

declare function test(options: Options): void

test({a: true})
// Success

const x = {
  a: true
}

test(x)
/** [ts] Error
 * Argument of type '{ a: boolean; }' is not assignable to parameter of type 'Options'.
 *   Types of property 'a' are incompatible.
 *     Type 'boolean' is not assignable to type 'true'.
 */

const y: Options = {
  a: true
}

test(y)
// Success

@hronro hronro changed the title type inference lost literal type of a enum type inference lost literal type Feb 4, 2019
@jack-williams
Copy link
Collaborator

@LuminescentMoon Yes, that is an exact manifestation of the problem. The true in x gets widened to boolean. With recent changes you can write:

const x = {
  a: true
} as const;

@hronro
Copy link
Author

hronro commented Feb 8, 2019

@jack-williams
Is this available in TypeScript v3.3?

@dragomirtitian
Copy link
Contributor

@foisonocean Will be available in 3.4 (you can get it now to play with using npm install typescript@next). The PR: #29510

@Muon
Copy link

Muon commented Aug 23, 2019

Here's a MWE I just reduced:

enum X {
  A,
  B
}

interface Y {
  kind: X.A;
}

const foo = { kind: X.A };
// error: Type '{ kind: X; }' is not assignable to type 'Y'.
//  Types of property 'kind' are incompatible.
//    Type 'X' is not assignable to type 'X.A'.
const bar: Y = foo;

Is there a particular reason that literals have their types widened by default?

@fent
Copy link

fent commented Nov 1, 2019

Here it is further reduced

const fun = (a: 'one') => { };
fun('one'); // <- this is fine
let a = 'one';
fun(a); // <- error here

using as const is not a fix imo. we can see here on the last line that calling fun(a) shouldn't error. the compiler could theoretically infer a as both of type 'a' and of a subtype string, and check both types when assigning to another variable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

8 participants