-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Type literal assertion as const
to prevent type widening
#26979
Comments
I think this is a duplicate of #10195. |
@mattmccutchen You're right, #10195 proposes a different solution for the same problem. However I think this one could be implemented much more easily as it doesn't have any breaking changes and doesn't colide with something as basic as... parentheses. |
as const
to solve pain in object literal definitionas const
to prevent type widening
"solution" that works today: const data = new class {
readonly name = "Foobar";
readonly favouritePrime = 7;
readonly sports = [ 'chess' ];
}; |
I suggested pretty much the same thing in #10195 (comment), but with |
One example of how this comes up in common code (if I am understanding the issue correctly) - React CSS styles.
|
The React CSS styles can be handled with a type annotation on the constant: const style: React.CSSProperties = {
textAlign: 'center'
}
return <div style={style}></div> |
This is somewhat possible with the following notation: // doesn't quite work right for interface types:
type Exactly<T> = T | never;
// for value types:
// unfortunately you have to echo v into the type param if you want to constrain it to only that value
const Literally = <T>(v: T): Exactly<T> => {
return v as Readonly<T>;
}; // In objects
const o = {
a: Literally<42>(42),
b: Literally<"foo">('foo'),
c: Literally<'someReallyLongPropertyValue'>('someReallyLongPropertyValue'),
d: Literally(Symbol('a'))
};
// In generic function
let f = <T>(a: T) => ({ boxed: a });
let x = f(Literally<42>(42));
x.boxed = 43; // compile time error |
@gwicksted your code can be simplified as follows (note no need to repeat all the literals twice): function literal<T extends string|number|symbol>(v: T): T {
return v;
};
// In objects
const o = {
a: literal(42),
b: literal("foo"),
c: literal('someReallyLongPropertyValue'),
d: literal(Symbol('a'))
};
// In generic function
let f = <T>(a: T) => ({ boxed: a });
let x = f(literal(42));
x.boxed = 43; But note this only works for string and number literals. Symbols, tuples and booleans are widened (they are widened with your code too). |
@yortus That's great! It seems the trick is the "extends" and one of the literal types with the notable exceptions you mentioned. Booleans, Tuples, numbers, and strings all work when given the specific type using Full example: function literal<T extends string|number>(v: T): T {
return v;
};
// In objects
const o = {
a: 42 as 42, // or literal(42)
b: "foo" as "foo", // or literal("foo")
c: literal("someReallyLongPropertyValue"),
d: Symbol("a"), // no solution yet
e: false as false,
f: ["abc", false] as ["abc", false]
};
o.a = 43; // error
o.b = "bar"; // error
o.c = "shorter"; // error
o.d = Symbol("d"); // no error
o.e = true; // error
o.f = ["def", true]; // error
// In generic function
let f = <T>(a: T) => ({ boxed: a });
let x = f(literal(42));
x.boxed = 43; // error IMO it would be great if something as simple as this would prevent widening in all cases: // PROPOSAL: does not work
function literal<T extends string | number | boolean | symbol>(v: T): T | never {
return v; // as T | never
} |
@gwicksted @yortus This is a cool walkaround, but it affects the compiled JavaScript. However I guess the interpreter will quickly realize it's an identity function and optimize the heck out of it. |
EDIT: figured out tuples of any length. Here's a slightly more refined // Overloaded function - supports strings, numbers, booleans and tuples. Rejects others.
function literal<T extends string | number | boolean>(t: T): T;
function literal<T>(t: Tuple<T>): T;
function literal(t: any) { return t; }
type Tuple<T> = T extends [any?, ...any[]] ? T : never;
let v1 = literal(42); // 42
let v2 = literal('foo'); // 'foo'
let v3 = literal(true); // true
let v4 = literal([]); // []
let v5 = literal([10, 20]); // [number, number]
let v6 = literal([literal(1), false, 'foo']); // [1, boolean, string]
let v7 = literal([1, 2, 3, literal('foo'),
true, literal(true)]); // [number, number, number, 'foo', boolean, true]
let v8 = literal(Symbol('sym')); // ERROR For boolean literals, this depends on #27042 which arrived in the For tuples, |
@yortus nice! It's succinct and hits the 99% with a single function. I think it solves #10195 without compiler changes (just lib). Not sure if it helps with #26841 On that note: should It's unfortunate things become verbose with a complex Tuple structure (but those are rare in my experience): const o: {
f: ["abc", false, [123, true]] as ["abc", false, [123, true]] // this works
g: literal([literal("abc"), literal(false), literal([literal(123), literal(true)])])
};
o.f = ["abc", false, [567, false]]; // error on both [567, false]
o.g= ["abc", false, [567, false]]; // (untested, as I do not have dev nightly at the moment) Can it be written like the following and still work? Then type Tuple<T> = T extends [any?, ...any[]] ? T : never;
function literal<T extends string | number | boolean | Tuple<T>>(t: T): T { return t; } |
Search Terms
String literal type and number literal type in an object literal. Type assertion to make the string literal or number literal also a type literal. Cast string or number to type literal. Make a property definition in object literal behave like constant for the type inference. Define a property as string literal or number literal in an object. Narow the type of a literal to its exact value.
Background
When I declare a constant and assign a string literal or a number literal to it, type inference works differently than when I declare a variable in the same way. While the variable assumes a wide type, the constant settles for a much narrower type literal.
This behavior comes in handy in many scenarios when exact values are needed. The problem is that the compiler cannot be forced to the "type literal inference mode" outside of the
const
declaration. For example the only way of defining an object literal with type-literal properties is this:That is not only incredibly annoying, but also violates the principles of DRY code. In adition it's really annoying.
Suggestion
Since
const
is already a reserved keyword and can't be used as a name for a type, adding a type assertion expresionas const
would cause no harm. This expression would switch the compiler to the "constant inference mode" where it prefers type literals over wide types.Non-simple types
There are currently three competing proposals for the way
as const
would work with non-simple types.Shallow literal assertion
The first one would stay true to its name and would treat the type as if it were assigned to a constant.
However, this would mean that all non-trivial expressions would stay the same as they were without the
as const
. Therefore I would argue it's less useful than the other two proposals.Tuple-friendly literal assertion
The second proposal differs in the way it treats array literals. While the shallow assertion treats all array literals as arrays, the tuple-friendly assertion treats them as tuples. Then it recursively propagates deeper untill it stops at a type that is neither a
string
,number
,boolean
,symbol
, norArray
.This is probably the most useful proposal, as it solves both the problem described in Use Cases and the problems described in #11152.
Deep literal assertion
The third proposal would recursively iterate even through object literals. I included it just for sake of completeness, but I don't think it could be any more useful than the tuple-friendly variant.
Use Cases
Say I'm using a library which takes a very long object of type
LibraryParams
of various parameters and input data. I don't know some of the data right away, I need to compute them in my program, so I'd like to create my objectmyParams
which I would fill and then pass to the library.Since some of the properties of
LibraryParams
are optional and I don't want to check for them or assert them every time I use them – I know I've set them, right? – I wouldn't set the type ofmyParams
toLibraryParams
. Rather I'd use a narrower type by simply declaring an object literal with the things I need.However some of the properties need to be selected from a set of exact values and when I add them to
myParams
, they turn into astring
or anumber
and render my object incompatible withLibraryParams
.There are some ways around it using the existing code, none of which are particularly good. I'll give some examples in the next section.
Examples
Imagine that all of these examples contain much longer programs.
Checklist
My suggestion meets these guidelines:
Related
#14745 Bug that allows number literal types to be incremented
#20195 A different approach to the problem of type literals in objects, excluding generic functions
#11152 Use cases for
as const
in generic functionsThe text was updated successfully, but these errors were encountered: