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

Syntax for hinting literal type inference #10195

Closed
zpdDG4gta8XKpMCd opened this issue Aug 7, 2016 · 102 comments · Fixed by #29510
Closed

Syntax for hinting literal type inference #10195

zpdDG4gta8XKpMCd opened this issue Aug 7, 2016 · 102 comments · Fixed by #29510
Labels
Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Aug 7, 2016

Now that we have so many literal types we more than ever need new syntax that would make their use natural. Please consider the following:

const value = (true); // true
const value = true; // boolean

const value = ('a'); // 'a'
const value = 'a'; // string

const value = (1); // 1
const value = 1; // number

const value = ['a', 1]; // (string | number)[]
const value = (['a', 1]) // [string, number]
const value = ([('a'), (1)]) // ['a', 1];

Problem:

  • There is no way to get a literal type value without explicit type annotation.

Solution:

  • Add new syntax (or shall we say some new semantics to the old mostly unused syntax)

Highlights:

  • Composable.
  • Works fine with the existing syntax, doesn't need a new one.
  • Highly unlikely to be a breaking change.
  • Doesn't affect JavaScript semantics at all, can be executed as written and will work as expected.

Shortcomings:

  • Undiscoverable syntax.
  • Semantic conflicts (breaking changes) although rare:
    • generated code
    • conditional expressions
    • unintended excessive syntax

Prior work:

@SimonMeskens
Copy link

SimonMeskens commented Aug 7, 2016

Can you explain what the problem with this syntax is? Too verbose? This works in Typescript today.

interface Tuple {
    [0]: string;
    [1]: number;
}

var value: Tuple = ['a', 1]; // type checks correctly

In fact, just tested and this works too:

interface Tuple<U, T> {
    [0]: U;
    [1]: T;
}

var value: Tuple<string, number> = ['a', 1];

I assume the problem here is that you need to manually annotate the value? You want sane implicit typing of literals?

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 7, 2016

  1. Problem:
    There is no way to get a literal type value without explicit type annotation.

    image

  2. yes

  3. yes

  4. yes

@SimonMeskens
Copy link

SimonMeskens commented Aug 7, 2016

Ah, gotcha. Yeah, I hardly ever use implicit types, I manually type everything explicitly. I get the need for this proposal, but I personally think the proposed syntax is unexpected behavior. I don't think it will break anything, but there seem to be a lot of edge cases and as a user, I don't expect (true) to type differently than true. I don't like surprises in my type checking.

Personally, I would just write this and make the intent explicit:

const value = ['a', 1] as [string, number];

Of note, you can't use literal types in "as" notation, which I would propose to fix this issue personally:

const value = true as true;

@zpdDG4gta8XKpMCd
Copy link
Author

could you give me an example when you casually type (true), please?

@zpdDG4gta8XKpMCd
Copy link
Author

point is it's the waste of syntax, no one types (true) in their everyday work, why not to give it a better use?

@SimonMeskens
Copy link

A quick search of my code base of the previous big project I did in Typescript gives me this:

var borderWidth = (4);

...

borderWidth = 12;

I assume this breaks with your proposal? In which case, yes, your proposal would break the last big codebase I worked on. Why are those parenthesis there? I assume at one point it said something like borderWidth = (4 * someOtherValue);. Real life code bases are messy, someone forgot to take those parenthesis out. I wouldn't mind something like that breaking with an update of TypeScript btw, just saying that people do casually type stuff like that.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 7, 2016

you are lucky to find one place that, well, was left unattended, rather than crafted the way it is on purpose, and it's just one scalepan... - your unintentionally overlooked code, the other scalepan is a new feature that enables the whole new world of exciting opportunities and universal happiness, now what exactly are we arguing about?

@SimonMeskens
Copy link

We're not arguing, I said from the beginning I like the idea of the feature 😉

I'm just not sure if the proposed syntax is to my liking, seems a bit unexpected. Then again, as I said, I don't use implicit typing, so it really doesn't mean much to me.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 7, 2016

then give me some thumb's up's!

unexpectedness of the syntax is already spotted (in the parent proposal), admitted and listed here under "Shortcomings"

i confess i lived a sinful life, the proposal is not 100% perfect

@SimonMeskens
Copy link

Just one more observation: if I have to manually type those parenthesis, then I'm explicitly annotating that literal. I don't think your proposal is implicit annotation at all, it's just a shorthand explicit annotation. The shorthand is universal and saves you from explicitly mentioning the type, but it's explicit regardless. Anyway, I'm knee deep in physics integrators right now, time to get back.

@alitaheri
Copy link

alitaheri commented Aug 7, 2016

hmmm how about a new operator? := This way, if the compiler, by any chance can infer the value it will explicitly assing the type.

const a := 1; // a is 1

// Can work with expression
const b := (Math.random() * 0); // b is 0
// As it won't be confused with explicit parentheses like:
const b = (Math.random() * 0) + 1; // b is number even though b is always 1

const c := Math.random(); // c is number

const d := 'hello'; // d is 'hello'

const e := !false; // e is true

:= kinda aliases explicit type annotation const a: 1 = 1 => const a := 1, the syntax is new and kinda expected 😁

@zpdDG4gta8XKpMCd
Copy link
Author

well yeah, i never said i wanted it implicit, all i want it to get rid of

... explicit type annotations

while still being explicit about my intentions at defining a literal value

@SimonMeskens
Copy link

Yup, I get it now. I looked over your initial thread too. I'll give this a thumbs up, I do think it would be useful and it does have a parallel to the arrow syntax.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 7, 2016

@alitaheri good catch!

i agree the condition expression of the ternary operator is usually tend to be braced in parenthesis

here is the thing though:

  • we don't care about any expressions other than literal ones

a literal expression is an expression whose terms DO NOT contain variables

i hate the flatness of this statement but nevertheless it would enable what's required it if accepted

@zpdDG4gta8XKpMCd
Copy link
Author

@alitaheri

:= is limiting it to assignment cases only

how about binding arguments to parameters?

function id<a>(value: a): a { return value; }
const value = id('hey');

@alitaheri
Copy link

@Aleksey-Bykov Yeah good point, I guess inferring value isn't that simple anyway 😅 and assignment-only operator will limit the usage a lot! specially with tuples O.o

@Artazor
Copy link
Contributor

Artazor commented Aug 7, 2016

I'm supporting the idea that we need a syntax for the literals (as well as for literal tuples, literal records). However, I'm seriously in doubt about ( ). Mostly because of code generation tools. The analogy between {} and ({}) in the body of the arrow function is incorrect: in the case of arrow functions we have simply parsing disambiguation hint between Expression and BlockStatement.
What you are proposing is completely different. We know that ( are ) are used only for grouping at the expression level.

@Artazor
Copy link
Contributor

Artazor commented Aug 7, 2016

And if you'll change a semantic of the ( ) - you have chances to seriously interfere with existing code generation tools and approaches.

@Artazor
Copy link
Contributor

Artazor commented Aug 7, 2016

I'd propose a diamond operator <> as the most concrete type.

var a = <>1;
var a = 1 as <>;

@Artazor
Copy link
Contributor

Artazor commented Aug 7, 2016

or even an empty type annotation.

var a = <>1;
var a = 1 as ();

However, both variants are ugly :(

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 7, 2016

@Artazor, it's a good point too that you mentioned...

a couple of thoughts:

  • cases like (a + b) * c are what the grouping expression is intended for, we are not trying to mess with them, as i said it only matters for literal expressions, so we are safe here
  • case like (1 + 2) are, well, although possible, but, pardon my arrogance, quite rare AND! we can be still statically correct here, meaning we can infer 3 from that expression if it matters

what it comes down to is the willingness to give a better purpose to some mostly unoccupied syntax

@Artazor
Copy link
Contributor

Artazor commented Aug 7, 2016

Just to clarify: (1 + 2) and (1) are rare in the hand-written code, but is pretty often observable in a generated code (macro-expansions and so on), that was my concern.

Are the following three code snippets equivalent?

var a = (1);
const ONE = 1 as 1;
var a = (ONE);
import {ONE} from "./constants"; // assume export const ONE = 1 as 1
var a = (ONE);

@aluanhaddad
Copy link
Contributor

@Aleksey-Bykov I appreciate the need for this and the generality of this proposal as compared to #9217, but I share the concerns expressed by @Artazor. I think the use of parens to indicate these types is very problematic. I suggest simply replacing them with < and > which would then be removed from the emitted JavaScript. At that point I would be all for this proposal.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 7, 2016

< and > already have very distinct purpose:

  • type assertions: <1>1
  • html tags: <true>false</true>

i am not ready to claim there will be a conflict but my gut filling says that it will complicate parsing at least

@zpdDG4gta8XKpMCd
Copy link
Author

Are the following three code snippets equivalent?

Yes.

var a = (1); // "a" is 1: new syntax at play

Yes.

const ONE = 1 as 1; // ONE is already a literal number
var a = (ONE); // "a" is 1: since we have a constant (not a literal) the parenthesis are ignored here, but the type of the expression is still literal number as stated above

Yes.

import {ONE} from "./constants"; // assume export const ONE = 1 as 1
var a = (ONE); // "a" is 1: very much like the previous case, importing doesn't change anything

@Igorbek
Copy link
Contributor

Igorbek commented Aug 7, 2016

Have the consequences of having implicit literal types on const targets been investigated?
I expect no problems with that. If it were introduced that it could be like this:

var one = 1; // number
const two = 2; // literal type 2
var three = two + 1; // ok, number
var otherTwo = two; // now, it's literal type 2, which is undesirable. Maybe downcast it to number here?

On other hand, do we really need it? Why would you need to implicitly type literal type?
From my experience, that is common to have literal types references on interfaces in order to discriminate them. So that means, you would already have explicitly annotated literal type.

@Aleksey-Bykov do you have any real world examples in mind?

@zpdDG4gta8XKpMCd
Copy link
Author

not sure what you are talking about, there wasn't anything said about implicitness (everything is strictly explicit)

in your example

const two = 2;

the constant two is of type number, not 2

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 7, 2016

we don't need implicit typing, we need number literal types, for example:

  • instead of saying: type Direction = number we say type Direction = -1 | 1; the benefit of going with the latter is eliminating a chance to get 0 which doesn't have any meaning where only -1 or 1 are expectd

@Igorbek
Copy link
Contributor

Igorbek commented Aug 7, 2016

const two = 2;

I meant if const targets were implicitly typed by literal types then two would be of type 2.

@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Aug 7, 2016

again, there was absolutely nothing said about implicit typing, we are talking about a special syntax to explicitly specify that the type of a numeric literal is a literal type (1 or 2 or ...) rather than a number

@KiaraGrouwstra
Copy link
Contributor

@demurgos I'd argue specific types shouldn't require additional effort as type widening is mostly useful under specific circumstances (mutable variable, i.e. var/let assignment), meaning we already have a decent idea what default makes sense when.

For example @(1 + 2) would be typed as 3.

Seems they didn't like this, see #15645.

@forivall
Copy link

forivall commented Mar 14, 2018

While it only works for a limited amount of cases, I would suggest overloading the ! postfix operator when used on literal value expressions; since we know that literally 1! would never be nullable, this now means that it's exactly one. I would also think that this has lower impact than the parens idea, since the postfix ! is already a typescript-only syntax, and the only people writing 1! would be doing it as a typo.

So, some examples using postfix !
(filtering out those that have been solved by #10676)

const value = ['a', 1]; // (string | number)[]
const value = ['a', 1]!; // [string, number]
const value = ['a'!, 1!]!; // ['a', 1]
const value = ['a'!, 1!]; // ('a' | 1)[]

const value = {a: 1} // {a: number}
const value = {a: 1!} // {a: 1}

cases that it doesn't solve

const foo = 'foo' // 'foo'
const bar = [foo!]! // would still be [string]
const value = {a: foo!} // still {a: string}

The other syntax solution I can think of (since I like keywords more than characters :P ) is to use as const as a postfix, for example

const value = {a: foo as const}

Or, maybe both? Allow postfix ! to narrow when it's a literal, and as const for more complex mappings?

Edit (2019-01-14): @m93a as submitted this as a separate issue as #26979

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Mar 14, 2018
@zpdDG4gta8XKpMCd
Copy link
Author

zpdDG4gta8XKpMCd commented Mar 14, 2018

good thing is that we can piggyback ride on the existing TypeScript only expression level syntax ! (which is so much at odds with its design goals but who cares right?)

bad part is that there is no way to see what is going on in const value = {a: foo!} without knowing what foo is, it's going to be a nightmare for code reviewers like myself

@RyanCavanaugh
Copy link
Member

My interpretation was that ! would only have the literalizing effect on true literal expressions; even ("foo")! should be a no-op IMO. Otherwise you get into a ridiculous situation when expr: "foo" | null - do you then have to write expr!! to prevent it widening?

@forivall
Copy link

Yup, exactly what Ryan is saying. The ! would only apply on literals, and so const value = {a: foo!} would unambiguously be a non-null assertion.

@falsandtru
Copy link
Contributor

@forivall I coincidentally opened an issue about that syntax. Could you discuss about that in #22872 if you prefer?

@aminpaks
Copy link
Contributor

aminpaks commented Dec 8, 2018

This is great, why can't we focus on just string literal type for now?

let value = 'myType'; // string
let value = `myType`; // 'myType'
const value = ['myType']; // string[]
const value = [`myType`]; // 'myType'[]
const value = {a: 'myType'} // {a: string}
const value = {a: `myType`} // {a: 'myType'}

type myType = 'myType'; // OK
type myType = `myType`; // Error
let myType = `myType`; // OK 'myType'

@aloifolia
Copy link

aloifolia commented Jan 13, 2019

So what is the current state of this issue? I just checked some examples and noticed that "primitive" expressions seem to be inferred as literal types. Expressions which yield objects, however, stick to the more general types. Also, the compiler distinguishes between constants and variables.

Examples:

// ✓ type 1
const num1 = 1;

// ✓ type number
const 

// ✓ type b => 1 | 2
function test(b: boolean) {
    return b ? 1 : 2;
}

// 😕type string[]
const strs = ['a']

// 😕type { key: string }
const obj = {
    key: 'value'
}

So I guess, the reasoning behind the current inference is: Can this value be changed?

  • if so, then use the more general type
  • if not, then the more specific type is safe to assume

Maybe the problem could be mitigated by telling the compiler more precisely which objects (i.e. standard objects and arrays) are actually constant and which should be changeable. Thus, the as const proposal by @forivall seems to be a reasonable solution (although a bit cumbersome to use - which could be eased by having some sort of top-down cascading behaviour for, say as const!).

Note that I am using Typescript 3.2.2.

@pelotom
Copy link

pelotom commented Jan 13, 2019

@aloifolia

Maybe the problem could be mitigated by telling the compiler more precisely which objects (i.e. standard objects and arrays) are actually constant and which should be changeable. Thus, the as const proposal by @forivall seems to be a reasonable solution (although a bit cumbersome to use - which could be eased by having some sort of top-down cascading behaviour for, say as const!).

I proposed something like this too in #20195, which is to extend the readonly operator to be applicable to object (and array) literals, e.g.

const o = readonly { x: 3, y: 'hello' };
// o: { readonly x: 3; readonly y: 'hello' }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: Literal Types Unit types including string literal types, numeric literal types, Boolean literals, null, undefined In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.