Closed zpdDG4gta8XKpMCd closed 5 years ago
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" notifications@github.com 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; // 1
refs 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.bykov@gmail.com wrote:
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" notifications@github.com 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; // 1
refs 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'
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:
Problem:
Solution:
Highlights:
Shortcomings:
Prior work: