microsoft / TypeScript

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

Syntax for hinting literal type inference #10195

Closed zpdDG4gta8XKpMCd closed 5 years ago

zpdDG4gta8XKpMCd commented 8 years ago

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:

Solution:

Highlights:

Shortcomings:

Prior work:

SimonMeskens commented 8 years ago

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 commented 8 years ago
  1. Problem: There is no way to get a literal type value without explicit type annotation.

    image

  2. yes
  3. yes
  4. yes
SimonMeskens commented 8 years ago

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

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

zpdDG4gta8XKpMCd commented 8 years ago

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

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

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

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

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

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

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

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 :grin:

zpdDG4gta8XKpMCd commented 8 years ago

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

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

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

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

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

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

Artazor commented 8 years ago

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

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

Artazor commented 8 years ago

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

var a = <>1;
var a = 1 as <>;
Artazor commented 8 years ago

or even an empty type annotation.

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

However, both variants are ugly :(

zpdDG4gta8XKpMCd commented 8 years ago

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

a couple of thoughts:

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

Artazor commented 8 years ago

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

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

< and > already have very distinct purpose:

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

zpdDG4gta8XKpMCd commented 8 years ago

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

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

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

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

Igorbek commented 8 years ago
const two = 2;

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

zpdDG4gta8XKpMCd commented 8 years ago

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

zpdDG4gta8XKpMCd commented 8 years ago

consider:

const two = 2; // two is number (traditional TS)
const two : 2 = 2; // two is 2 (a new literal numeric type since TS 2.0.1 which hasn't been yet released)
const two = <2> 2; // another way of saying two is 2 (also valid in TS 2.0.1)
const two = (2); // the PROPOSED way of saying the same, hence the discussion
Igorbek commented 8 years ago

If you are not annotating type explicitly then it will be inferred implicitly :)

const one: 1 = 1; // explicit
const two: (2); // or whatever syntax you want, implicit
zpdDG4gta8XKpMCd commented 8 years ago

i think that you might need to read the following to refresh your memory about literal types: https://github.com/Microsoft/TypeScript/pull/9407

SimonMeskens commented 8 years ago

@Igorbek, he is correct, if the only reason a user should use parens is to indicate literal type, then it is explicit typing (think of it as a shorthand for "1 as 1").

@aleksey-bykov, he has a point, that if we make it so the compiler will always choose the most specific type for const values, there is no need for your shorthand at all. We could just tell the compiler to always choose literal types when dealing with a const (the most specific type) and your issue goes away entirely (unless you also want to use this new syntax for regular vars, but I don't think you do).

Igorbek commented 8 years ago

@SimonMeskens thank you for mediation :)

zpdDG4gta8XKpMCd commented 8 years ago

So there are 2 sorts of types: the red ones and the blue ones. Currently the red ones are dominating by being the only ones to have a literal representation for their values. The blue ones are oppressed by not having a way to get their values declared without explicit annotations. This is a horrible example of discrimination we say. Let's turn it the other way around by letting the blue ones have their values right off the bat while requiring the annotation for the red ones, so that they know what it feels like to be humiliated, right? Can we celebrate yet? Looks like we just solved the problem, didn't we?

Now when we say: var x = 1; we can be sure that we protected ourselves from all sort of nonsense like x = 2; or x = x + 1;

Case closed, class dismissed

On Aug 7, 2016 7:27 PM, "Simon Meskens" notifications@github.com wrote:

@Igorbek https://github.com/Igorbek, he is correct, if the only reason a user should use parens is to indicate literal type, then it is explicit typing (think of it as a shorthand for "1 as 1").

@aleksey-bykov https://github.com/aleksey-bykov, he has a point, that if we make it so the compiler will always choose the most specific type for const values, there is no need for your shorthand at all. We could just tell the compiler to always choose literal types when dealing with a const (the most specific type) and your issue goes away entirely (unless you also want to use this new syntax for regular vars, but I don't think you do).

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Microsoft/TypeScript/issues/10195#issuecomment-238115680, or mute the thread https://github.com/notifications/unsubscribe-auth/AA5PzQ9ADqDtD8X4g7Y_erSBaXXiq_7fks5qdmn5gaJpZM4Jei5a .

zpdDG4gta8XKpMCd commented 8 years ago

My point is that depending on the situation we need either one sort of types or another. Effectively we need them both equally bad. The current way of doing ones is too clumsy. And any solution that favors one over another is equal awkward. The problem with picking the most limiting type is 1. a massive breaking change, 2. not always the desired behavior, 3. doesn't fix anything just articulates the same problem in a different way

If there is a solution, it should make the use of both the traditional and literal types equally comfortable, ideally without breaking much stuff along the way.

SimonMeskens commented 8 years ago

I can't see a single use case where a more specific const breaks anything. You can't change it, you can only use it and you can use a more specific type any place you can use the more general type. Const values should always be as specific as possible. Could you give an example of code that breaks by making a const value a more specific type?

zpdDG4gta8XKpMCd commented 8 years ago

Couple of cases that aren't immediately clear with picking most limiting type:

const zero = [0, 0]; // what do we get here?
var direction = Math.random () > 0.5 ? -1 : +1; // how can i flip it?
Igorbek commented 8 years ago

zero is [0, 0], direction is number (I'd prefer that for vars) or -1 | 1 (stricter)

zpdDG4gta8XKpMCd commented 8 years ago
const at = zero;
at[0] += 1; // shall we break?

function flip(direction: -1 | 1): -1 | 1 {}
var direction = Math.random () > 0.5 ? -1 : 1;
direction = flip (direction); // shall we break?
zpdDG4gta8XKpMCd commented 8 years ago
function toBacked<a> (defaultValue: a) {
    return function backed (value: a) {
         return value || defaultValue;
    }
}
const backed = toBacked (1);
const x = backed (0); // shall we break?
yortus commented 8 years ago

Since they are called unit types, what about this:

// These variables will have the literal type of the given value
var pi = <unit> 3.14;
var option = 'strictNullChecks' as unit;
const TRUE = <unit> true

Casting to unit means taking the unit type of the literal value being cast.

Reasoning:

SimonMeskens commented 8 years ago

I don't get your examples aleksey. None of them use literals:

const zero = [0, 0]; // not a literal, irrelevant
var direction = Math.random () > 0.5 ? -1 : +1; // var is not const, example is irrelevant
const backed = toBacked (1); // not a literal, irrelevant
aluanhaddad commented 8 years ago

< and > already have very distinct purpose:

type assertions: <1>1 html tags: false i am not ready to claim there will be a conflict but my gut filling says that it will complicate parsing at least

@aleksey-bykov just to be clear, I was proposing

const one = <1>;

as a syntactic sugar for the forms

const one = 1 as 1;

and

const one = <1>1;

and

const one: 1 = 1; // different but has the same effect in this context

I do not believe this conflicts with anything but you are correct that it would complicate parsing with JSX enabled.

zpdDG4gta8XKpMCd commented 8 years ago

@SimonMeskens

If I understood the idea about using the most limiting type for numbers, the following rules will govern it:

By following these 3 rules we are looking to achieve the following goals:

The following are the examples demonstrate the shortcomings of it:

Example 1:

Breaking change (used to work as written before R1)

const at = { x: 0, y: 0 }; // according to R1 "at" has the type "{ x: 0, y: 0 }"
// the result of the following expression is a "number" according to R3
at.x +=1;  // compiler error: since "x" is always "0" it cannot be assigned a "number"

Example 2:

The annotations are still required in many cases like the following one, so G1 is not achieved

function flip(direction: -1 | 1): -1 | 1 { return direction > 0 ? -1 : 1; }
// here we need to mutate a variable of type "-1 | 1" which we wish
// could be inferred from the expression thank to R1 but cannot be due to R2,
var direction = Math.random () > 0.5 ? -1 : 1; // direction is number
// so a call to the "flip" function would give a compilation error:
direction = flip(direction); // cannot use a number where "-1|1" is expected
// in order to make this example work we need explicitly annotate either
// the "direction" or the number literals, which undermines the whole idea

Example 3:

Breaking change (used to work as written before R1)

function toBacked<a>(defaultValue: a) {
    return function backed(value: a) { return value || defaultValue; };
}
const backed = toBacked(1); // thank to R1 the function "backed" is of type "(value: 1) => 1"
const x = backed(0); // compiler error, "0" cannot be used where "1" is expected
zpdDG4gta8XKpMCd commented 8 years ago

@yortus, i like your idea a lot, it might be worth creating a separate request for it

@aluanhaddad, the following will suffer from the same problems as HTML syntax markup in JSX templates conflicting with type assertions: https://github.com/Microsoft/TypeScript/issues/296

const one = <1>;
DanielRosenwasser commented 8 years ago

For the record, parentheses used to "stop" contextual typing, but that was a horrible idea because it tripped a lot of people up. It was extremely subtle so I think it's out of the question.

Let the syntactic bikeshedding continue. :bike: :house:.

zpdDG4gta8XKpMCd commented 8 years ago

@DanielRosenwasser what about this one: https://github.com/Microsoft/TypeScript/issues/10195#issuecomment-238133905 ?

Artazor commented 8 years ago

It is not semantically correct to use the same type. These types are singletons, but they are distinct. The unit type is actually a void type.