Skip to content
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

Literal types don't work with generics as expected #12267

Closed
arusakov opened this issue Nov 15, 2016 · 12 comments
Closed

Literal types don't work with generics as expected #12267

arusakov opened this issue Nov 15, 2016 · 12 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@arusakov
Copy link
Contributor

TypeScript Version: master (cef9d85)

Code

function create<T, D>(type: T, data: D) {
  return { type, data };
}

const obj = create('x', 1);

Expected behavior:
obj has type { type: 'x', data: 1 }

Actual behavior:
obj has type { type: string, data: number }

@rozzzly
Copy link

rozzzly commented Nov 15, 2016

const obj = create<'x', number>('x', 1) is what you're looking for. Typescript assumes you just intended for 'x' to be a string, not a literal. So if you want it to be typed with a literal, you have to explicitly mark that during your function call.

@arusakov
Copy link
Contributor Author

arusakov commented Nov 15, 2016

@rozzzly
It likes old (TypeScript 2.0) literal types behaviour:

const x1 = 1;      // x1: number
const x2: 1 = 1;  // x2: 1

But in TypeScript 2.1 literal types are much cleaner and powerful:

const x = 1; // x: 1

I expect similar behaviour for generics (like in issue description).

@ahejlsberg
Copy link
Member

Unless there is some indication that you want the literal type preserved, TypeScript will widen the type when it is inferred for a mutable location (such as an object literal property). There are many ways you can indicate you want to preserve the literal type, including:

const X: 'x' = 'x';
const ONE: 1 = 1;
const obj1 = create<'x', 1>('x', 1);  // { type: 'x', data: 1 }
const obj2 = create('x' as 'x', 1 as 1);  // { type: 'x', data: 1 }
const obj3 = create(X, ONE);  // { type: 'x', data: 1 }

For more discussion see #11126.

@ahejlsberg ahejlsberg added the Working as Intended The behavior described is the intended behavior; this is not a bug label Nov 15, 2016
@aluanhaddad
Copy link
Contributor

aluanhaddad commented Nov 16, 2016

I'm slightly confused.

In the following

const X: 'x' = 'x'; // "x"

const o = create(X, 1);  // { type: "x", data: number };

the type of o.type is inferred as "x" via the type of X.

In the following, however,

const X = 'x'; // still "x"

const o = create(X, 1);  // { type: string, data: number };

the type of o.type is inferred as string seemingly because X is not annotated. This puzzles me because the type of X is exactly the same in both cases.

@ahejlsberg
Copy link
Member

@aluanhaddad The difference between a const with and without a type annotation is explained in detail in #11126.

@ahejlsberg
Copy link
Member

@aluanhaddad I should add that the intuitive way to think of this is that we will never widen a literal type that resulted from an explicit type annotation. We only widen implicit literal types.

@aluanhaddad
Copy link
Contributor

@ahejlsberg excellent. Thank you.

@aluanhaddad
Copy link
Contributor

@ahejlsberg I appreciate the clarification. It definitely makes sense to me now but on the whole this is an interesting and rather subtle behavior.
In the past (prior to this discussion) I would have flagged the explicit type annotation, say during a code review, as unnecessary. Obviously this is a matter of style and opinion but it is interesting that the annotation has this kind of second-order effect.

@weswigham
Copy link
Member

weswigham commented Nov 16, 2016

@aluanhaddad If you know what the domain of each type argument is supposed to be, specifying it like so will also cause literal types to be correctly inferred from literals:

function create<T extends string, D extends number>(type: T, data: D) {
  return { type, data };
}

const x = create('x', 2); // { type: "x", data: 2 };

It's only unconstrained generics which have the widening ambiguity issue.

@aluanhaddad
Copy link
Contributor

@weswigham thank you for that. I like how the use of constraints makes the code more clear and at the same time appropriately places the authority of determining the literalness of the return type with the callee.

@arusakov
Copy link
Contributor Author

arusakov commented Nov 16, 2016

@weswigham
Thank you for this generics example. Is this code just a hack or a bug or a normal code/behaviour, that I can use for now and future?

@ahejlsberg
Thank you and all team for literal types. But after widening/non-widening (#11126) I need to write more code with more types in common cases. Example:

const x: 1 = 1; // because I want use x as 1, for example, in object literals { prop: x }

const y = x; // y: 1
let z = x; // z: 1

In this example const y and let z have absolutely the same behaviour. It's very strange for me. I need to write let smth: number everywhere. Because if I don't want mutable behaviour I will simply write const smth.

@lefb766
Copy link

lefb766 commented Mar 4, 2017

@weswigham Wonderful! I found your example can be extended to take arbitary literal types 👍

type TypesCanBeLiteral = number | string | boolean;

function create<T extends TypesCanBeLiteral, D extends TypesCanBeLiteral>(type: T, data: D) {
  return { type, data };
}

const x = create('x', 2); //: { type: "x", data: 2 };
const y = create(1, true); //: { type: 1, data: true }

@mhegazy mhegazy closed this as completed Apr 21, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

7 participants