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:
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?
Problem:
There is no way to get a literal type value without explicit type annotation.
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;
could you give me an example when you casually type (true)
, please?
point is it's the waste of syntax, no one types (true)
in their everyday work, why not to give it a better use?
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.
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?
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.
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
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.
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:
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
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.
@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
@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');
@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
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.
And if you'll change a semantic of the (
)
- you have chances to seriously interfere with existing code generation tools and approaches.
I'd propose a diamond operator <>
as the most concrete type.
var a = <>1;
var a = 1 as <>;
or even an empty type annotation.
var a = <>1;
var a = 1 as ();
However, both variants are ugly :(
@Artazor, it's a good point too that you mentioned...
a couple of thoughts:
(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(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 matterswhat it comes down to is the willingness to give a better purpose to some mostly unoccupied syntax
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);
@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.
<
and >
already have very distinct purpose:
<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
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
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?
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
we don't need implicit typing, we need number literal types, for example:
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 expectdconst two = 2;
I meant if const targets were implicitly typed by literal types then two
would be of type 2
.
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
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
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
i think that you might need to read the following to refresh your memory about literal types: https://github.com/Microsoft/TypeScript/pull/9407
@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).
@SimonMeskens thank you for mediation :)
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" [email protected] 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
.
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.
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?
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?
zero
is [0, 0]
, direction
is number
(I'd prefer that for vars) or -1 | 1
(stricter)
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?
function toBacked<a> (defaultValue: a) {
return function backed (value: a) {
return value || defaultValue;
}
}
const backed = toBacked (1);
const x = backed (0); // shall we break?
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:
(true)
idea, arguably):=
idea)unit
keyword only appears in type positions so can't clash with any JavaScript meaning, either present or futureI 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
< and > already have very distinct purpose:
type assertions: <1>1
html tags:
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.
@SimonMeskens
If I understood the idea about using the most limiting type for numbers, the following rules will govern it:
number
var
or let
declarations its type widens to number
number
By following these 3 rules we are looking to achieve the following goals:
The following are the examples demonstrate the shortcomings of it:
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"
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
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
@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>;
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:.
@DanielRosenwasser what about this one: https://github.com/Microsoft/TypeScript/issues/10195#issuecomment-238133905 ?
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.
i am afraid that @yortus is proposing to piggy-back on the type assertions syntax, which means unit
is not even a type, it is just an instruction for the type-checker to consider the following literal being of a literal type
While I like how it reads, it'd be better if the as
keyword didn't have two semantically different uses in expression positions.
Actually I could not find any feasible solution, without violation of the rule "not to play at the expression level". The initial proposal looks the only way but it will hurt a code generation.
About implicit singleton inference I would use not a keyword, but some delimiter like *
var a = <*>123;
var b = 456 as *;
Still strange.
let a1 = <123>*
let a2 = * as 123
let b1 = <123>any
let b2 = any as 123
let c1 = <123>self
let c2 = self as 123
let d1 = <123>unit
let d2 = unit as 123
Let the syntactic bikeshedding continue. 🚲 🏠.
How about :=
as in
const one := 1;
it'd be better if the
as
keyword didn't have two semantically different uses in expression positions.
@DanielRosenwasser can you explain what you mean here? as
is already a type assertion operator, so what new meaning is introduced?
var foo = {/***/} as Foo; // type assertion to Foo, otherwise foo would be of type {/***/}
var pi = 3.14 as unit; // type assertion to unit type 3.14, otherwise pi would be of type number
It's still an ordinary type assertion, just a shorthand to avoid having to repeat the literal twice, to avoid things like this:
var CommandID: "'{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}'" = "'{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}'";
...which would still be valid, but could be written shorthard as either of the following:
var CommandID = <unit> "'{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}'";
var CommandID = "'{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}'" as unit;
The unit type is actually a void type.
@Artazor all singleton types are _unit types_ according to https://github.com/Microsoft/TypeScript/pull/9407#issue-162831239 since they have only one value. So 3.14
and "blah"
are unit types, and null
and undefined
are too.
It is not semantically correct to use the same type.
as @aleksey-bykov said unit
is not a type, its a shorthard syntax. It could only appear in a type assertion next to a literal value, and would mean _'assert to the unit type of this literal'_, saving you from writing it twice.
How about
:=
...
@aluanhaddad that was already suggested, and @aleksey-bykov pointed out it would only work in assignment statements, whereas there are other places it would be useful to assert an expression to a unit type.
Allowing unit
in type position would introduce a new reserved word that would be conflicting with user-defined type unit
. Not a big deal, but need to consider.
@Igorbek that's true. It would be a breaking change to introduce any new keyword in a type position. A symbol could be used for the equivalent thing (already suggested above), it just starts to look like symbol soup var $=<*>"$"
.
@wallverb your suggestions would require type-driven emit, which is an anti-goal with TypeScript. TypeScript's erasable type system means you can strip out the type annotations and (syntax extensions aside) be left with working JavaScript. Your examples, with type annotations stripped out, would become:
let a1 = *
let a2 = *
let b1 = any
let b2 = any
let c1 = self
let c2 = self
let d1 = unit
let d2 = unit
...which is clearly not correct, hence the need to emit different code based on the type annotations for this to work, which would make the type system non-erasable.
what about crazy idea of:
let a1 = 0u123 // type: 123
let b1 = u'123' // type: '123'
ts could erase it
Just throwing things into the air :)
@wallverb yeah I was thinking of something like that, but the problem is that it adds expression-level syntax (another TypeScript non-goal). It looks like a literal value expression, but it's really an expression plus a type annotation, and it could clash with some future JavaScript syntax.
That's why I though the best thing would be to add syntax only into the type annotation itself, which can't clash with JavaScript. But as @Igorbek pointed out, even that could clash with peoples' existing code. It's a tough one.
This is probably too verbose (but quite readable)... a variation that doesn't add any expression level syntax and can't clash with anyone's existing type names:
var pi = <unit type> 3.14;
var CommandID = "'{B96B3CAB-0728-11D3-9D7B-0000F81EF32E}'" as unit type;
The pi
example is actually longer than just writing <3.14> 3.14
, but at least it's DRY (so no copypasta problems).
Yea I like your proposal the most
<unit>1
1 as unit
<u>1
1 as u
// or any other short keyword
and we could have another flag
for people to opt in :D
Would love to hear @DanielRosenwasser feedback on the as
concern
Would love to hear @DanielRosenwasser feedback on the
as
concern
I'm personally against syntax that looks like it does one thing based on other uses in the language, but does something different. Module augmentations are the best example of this. They take the form of declare module "foo" { }
which is identical to the syntax for ambient module declarations, but depending on where they're declared, they mean something different. That is a terrible user experience and was a bad choice of syntax.
Here, you are changing something that originally meant "tell TypeScript the type of this should be unit
" to instead do what we're discussing here. I don't think it's quite as bad, but it's not exactly ideal.
Another idea:
Flag: --inferLiteralTypes
to opt in
let a = 'test' // Type: 'test'
let b = "test" // Type: string
let c = +5 // Type: 5
let d = 5 // Type: number
let e = +-5 // Type: -5
Just adding my 2c, I'd welcome such feature for creating Maps:
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
] as [string, number][]);
Without an explicit typecast, TS complains.
I'm personally against syntax that looks like it does one thing based on other uses in the language, but does something different.
@DanielRosenwasser Another example is as any
, which looks like a type assertion but actually turns off type checking.
let's leave any
alone: #9999
@DanielRosenwasser
This is the error I get:
src/map.ts(1,21): error TS2345: Argument of type '(string | number)[][]' is not assignable to parameter of type 'Iterable<[{}, {}]>'.
Types of property '[Symbol.iterator]' are incompatible.
Type '() => IterableIterator<(string | number)[]>' is not assignable to type '() => Iterator<[{}, {}]>'.
Type 'IterableIterator<(string | number)[]>' is not assignable to type 'Iterator<[{}, {}]>'.
Types of property 'next' are incompatible.
Type '(value?: any) => IteratorResult<(string | number)[]>' is not assignable to type '(value?: any) => IteratorResult<[{}, {}]>'.
Type 'IteratorResult<(string | number)[]>' is not assignable to type 'IteratorResult<[{}, {}]>'.
Type '(string | number)[]' is not assignable to type '[{}, {}]'.
Property '0' is missing in type '(string | number)[]'.
I use these typings from CoreJS https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/core-js/core-js.d.ts#L606, there is Iterable<[K, V]>
instead of [K, V][]
. Why is this causing problems and my explicit typecast of constructor argument fixes the error?
Another example is as any, which looks like a type assertion but actually turns off type checking.
@yortus any
is a type, so it's consistent in this. Your earlier proof makes the assumption that a type is only assignable to another if it is a subtype, but that's not how our type system works. See the spec sections on Subtypes and Supertypes and on Assignment Compatibility.
@zakjan Thanks for pointing this out! You actually helped me uncover two issues
Iterable
is not used for contextual typing (#10237)As a workaround for your issue, you can use the lib
flag in TypeScript 2.0 beta instead of core-js
declaration file where this seems to be fixed.
If you're not using TypeScript 2.0 beta, you can add the appropriate overload yourself in a d.ts
file for globals.
interface MapConstructor {
new <K, V>(entries?: [K, V][]): Map<K, V>;
}
@yortus but as any _is_ a type assertion.
@DanielRosenwasser Great, thanks a lot. Using lib compiler options helped for me.
Being able to do this would be kinda cool:
function addProp<T, U extends unit string, V>(obj: T, propName: U, propValue: V): T & {U: V} {
let newObj = clone(obj);
newObj[propName] = propValue;
return newObj;
}
let obj1 = {foo: 1, bar: 2}; // obj1 is {foo:number, bar:number}
let obj2 = addProp(obj1, 'baz', 3); // obj2 is {foo:number, bar:number} & {baz:number}
let obj3 = addProp(obj1, [], 4); // ERROR: `[]` is not a string literal
Since the U
type argument would only accept a literal string, which must therefore be known at compile-time, the compiler could allow you to build statically-known types with ordinary dynamic-looking code.
If we exclude tuples and only take immutable types perhaps no special syntax is needed:
const value = (true); // true
const value = true; // true
const value = ('a'); // 'a'
const value = 'a'; // 'a'
const value = (1); // 1
const value = 1; // 1
refs https://github.com/basarat/typescript-book/issues/165#issuecomment-246293918 /cc @danielearwicker :rose:
until recently it wasnt a case, now indeed literal values in immutable
positions are infered as literal type values
however it's still not a case for arguments and return values and a few
more situation, so there is still need for syntax
On Sep 12, 2016 7:14 AM, "Basarat Ali Syed" [email protected]
wrote:
If we exclude tuples and only take immutable types perhaps no special
syntax is needed:const value = (true); // trueconst value = true; // true
const value = ('a'); // 'a'const value = 'a'; // 'a'
const value = (1); // 1const value = 1; // 1refs basarat/typescript-book#165 (comment)
https://github.com/basarat/typescript-book/issues/165#issuecomment-246293918
/cc @danielearwicker https://github.com/danielearwicker 🌹—
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-246317472,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AA5PzdZDXlMqZy9angd_D0xRJvXOYZTlks5qpTQqgaJpZM4Jei5a
.
consider
const a = 2; // 2
let a = 2; // number
On Sep 12, 2016 7:20 AM, "Aleksey Bykov" aleksey.[email protected] wrote:
until recently it wasnt a case, now indeed literal values in immutable
positions are infered as literal type valueshowever it's still not a case for arguments and return values and a few
more situation, so there is still need for syntaxOn Sep 12, 2016 7:14 AM, "Basarat Ali Syed" [email protected]
wrote:If we exclude tuples and only take immutable types perhaps no special
syntax is needed:const value = (true); // trueconst value = true; // true
const value = ('a'); // 'a'const value = 'a'; // 'a'
const value = (1); // 1const value = 1; // 1refs basarat/typescript-book#165 (comment)
https://github.com/basarat/typescript-book/issues/165#issuecomment-246293918
/cc @danielearwicker https://github.com/danielearwicker 🌹—
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-246317472,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AA5PzdZDXlMqZy9angd_D0xRJvXOYZTlks5qpTQqgaJpZM4Jei5a
.
until recently it wasnt a case, now indeed literal values in immutable
Indeed
Awesome! :rose:
@basarat Please don't exclude tuples from this feature request. They are really useful and it is annoying that it is necessary to always explicitly typecast them from arrays.
function foo(bar: [number, number]): number {
return bar[0] + bar[1];
}
foo([1,2]); // error
foo([1,2] as [number, number]); // ok
Please don't exclude tuples from this feature request.
Was just trying to get the issue for const foo = "some string"
to work. Apparently that work is already done. Happened in these 15 days ¯\_(ツ)_/¯
while I was busy with https://github.com/alm-tools/alm/pull/181
So ignore everything I've said :heart:
@basarat it was literally the most recent commit (no pun intended). #10676.
thanks @yortus and @basarat. this should indeed be fixed by #10676.
@mhegazy i understand your urge to make progress and have as much items closed as possible, the PR that you mention only addresses a subset of the problem, i would not say it was resolved, would you consider reopening it please or shall i create a new one?
i understand your urge to make progress and have as much items closed as possible
Do not think the number of issues i close, or not, affects anything. so no urge here to disregard user comments.
i would not say it was resolved
I do not think we will be adding a new syntax to infer literal types. for the reasons outlined in https://github.com/Microsoft/TypeScript/issues/9217#issuecomment-226551698
I deally the compiler is smart enough to understand what is a "literal type" and what is not. I do believe #10676 has addressed this (with the exception of tuple types).
would you consider reopening
sure thing.
to make it clear, the parts that haven't been addressed:
For primitive types, https://github.com/Microsoft/TypeScript/issues/10863 tracks addressing the regression for type assertion (used to work before #10676).
If I may reopen this, now that we have generic defaults, I'd like to give a new exemple on why this feature could be useful:
interface Param<T = "value"> {
value: T;
}
function test<T = "value">(arg: Param<T> = {value: "value"} as any): Param<T>["value"] {
return arg.value || "value";
}
const t1 = test(); // Type: "value";
const t2 = test({value: "value"}); // Type: string
const t3 = test({value: "other"}); // Type: string
const t4 = test({value: "otherNameForMyValue" as "otherNameForMyValue"}); // Type: "otherNameForMyValue"
To be clear, the new thing that 2.3 brings is the typing of the t1
variable.
This is not shown in this simple example, but I am using this type as an identifier in a disjointed union, with a default value that is overridable by the user. The problem is that getting a string for the T
type is unacceptable since it would break the discriminant property, and the only way to go around it is to use this horrible cast syntax where I have to write my value twice.
I don't think it could be automatically inferred, but at the very least we should have something to make it easy to write.
I've seen a couple of ideas in this thread but none of them were very convincing. Maybe reusing a keyword like let foo = const "value";
or let foo = readonly "value"
could work ? Or perhaps could we circumvent the problem altogether and add new types to specify that a generic constraint asks for a literal type instead of a primitive, something like type Param<T extends StringLiteral> = {value: T}
, so that the compiler could always infer a literal type there. That does seem like more work than simply add a syntax sugar on the declaration though.
if you change function test
in your example to function test<T extends string = "value">
, it'll capture literal types properly, so that t2
will be "value"
and t3
will be "other"
.
@Igorbek Well that's true in that particular case, but let's take a look at another example that I just encountered:
function select<T, R extends {[P in ValueKey]: T}, ValueKey extends string = "code">(item: T, values: R[], options: {valueKey: ValueKey} = {valueKey: "code"} as any) {
return values.find(value => value[options.valueKey] === item);
}
select(1, [{code: 1, label: "hello"}]); // Works, ValueKey = "code" which is the default.
select(1, [{id: 1, label: "hello"}], {valueKey: "id"}); // Error, ValueKey = string, understands that values should be {[key: string]: number} and label is a string
select(1, [{id: 1, label: "hello"}], {valueKey: "id" as "id"}); // Works, ValueKey = "id"
Of course, if the compiler could infer the type properly that would be great, but I'm not really sure how possible this is, especially if we care about backward compatibility.
I hope my PR #17785 would address this, by allowing people to reuse the const
vs. let
distinction to indicate whether they want [1,2,3]
or number[]
. There's obviously no silver bullet (what of [number]
? (1|2|3)[]
?), so there will always be cases where you may need casts. I think where this PR adds value though is by increasing user control.
I saw some comments proposing syntax that would enable literal inference only for assignation (:=
), this is not enough. I type most of my declarations explicitly but I still had issues because of lack of literal type narrowing when I updated one of my libraries to use mapped types. Mapped types improved the "correctness" of the types by removing some any
s but require the types of the various objects to be better inferred.
Here is a minimal example exposing the issue:
// Lib part: Provides classes to build schemas and test them at runtime
interface MetaType<T> {
test(val: any): val is T;
}
// Represents a specific variant from an enum
class EnumLit<T> implements MetaType<T> {
variant: T;
constructor(enumVariant: T) {
this.variant = enumVariant;
}
test(val: any): val is T {
return val === this.variant;
}
}
// Represents an object with multiple properties, each with their own type
class Doc<T extends {}> implements MetaType<T> {
props: {[P in keyof T]: MetaType<T[P]>};
constructor(props: {[P in keyof T]: MetaType<T[P]>}) {
this.props = props;
}
test(val: any): val is T {
for (const k in this.props) {
if (!this.props[k].test(val[k])) { return false; }
}
return true;
}
}
// User code
enum AnimalName {
Duck,
Cat,
}
interface Duck {
name: AnimalName.Duck;
}
// This breaks because {name: MetaType<AnimalName>} is not assignable to {name: MetaType<AnimalName.Duck>}
// This worked previously because Doc.params was just `{[P in keyof T]: MetaType<any>}`
const $Duck = new Doc<Duck>({name: new EnumLit(AnimalName.Duck)});
// You have to explicitly state the generic parameter of EnumLit (really heavy due to repetition)
const $Duck2 = new Doc<Duck>({name: new EnumLit<AnimalName.Duck>(AnimalName.Duck)});
Complete error:
error TS2345: Argument of type '{ name: EnumLit<AnimalName>; }' is not assignable to parameter of type '{ name: MetaType<AnimalName.Duck>; }'.
Types of property 'name' are incompatible.
Type 'EnumLit<AnimalName>' is not assignable to type 'MetaType<AnimalName.Duck>'.
Types of property 'test' are incompatible.
Type '(val: any) => val is AnimalName' is not assignable to type '(val: any) => val is AnimalName.Duck'.
Type predicate 'val is AnimalName' is not assignable to 'val is AnimalName.Duck'.
Type 'AnimalName' is not assignable to type 'AnimalName.Duck'.
Regarding the syntax bikeshedding, the idea of parens is nice but I agree that it can be confusing and may break many code generation tools.
I'd propose an addition similar to the !
assertion operator: add a unary "literal type narrowing" operator. For example @
or #
are unused currently.
Here is a comparison of what the various propositions may look like in my example:
// Parens (not very readable)
const $Duck = new Doc<Duck>({name: new EnumLit((AnimalName.Duck))});
// Diamond
const $Duck = new Doc<Duck>({name: new EnumLit(<> AnimalName.Duck)});
// Unit
const $Duck = new Doc<Duck>({name: new EnumLit(<unit> AnimalName.Duck)});
// Prefix @
const $Duck = new Doc<Duck>({name: new EnumLit(@AnimalName.Duck)});
// Postfix @
const $Duck = new Doc<Duck>({name: new EnumLit(AnimalName.Duck@)});
// Prefix #
const $Duck = new Doc<Duck>({name: new EnumLit(#AnimalName.Duck)});
// Postfix #
const $Duck = new Doc<Duck>({name: new EnumLit(AnimalName.Duck#)});
This operator could also be applied to expressions to ask the compiler to resolve the most specific type.
For example @(1 + 2)
would be typed as 3
.
@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 as3
.
Seems they didn't like this, see #15645.
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
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
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?
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.
@forivall I coincidentally opened an issue about that syntax. Could you discuss about that in #22872 if you prefer?
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'
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:
````ts
// ✓ 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?
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.
@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, sayas 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' }
Most helpful comment