GeezerVison is showing github.com — Support Const Type Constraint · Issue #41114 · microsoft/TypeScript
Support Const Type Constraint #41114Description
Search Terms
Const Type Constraint Literal
Suggestion
Add new syntax const T or a global generic type Constant<T = any> = T extends const T ? T : never;
type ConstString = const string is same as type ConstString = Constant<string>.
- If
T extends string | number | boolean | symbol | bigint | null | undefined, requires T to be a constant.
- If
T is object or Array, requires every member of T to be a constant.
- If
T is enum, nothing to do, because enum is same as const enum
The difference with union literal is that type 'foo' | 'bar' can only be 'foo' or 'bar', but type const string can be any constant string.
Use Cases
- Sometimes we want to constrain a type or a parameter to be literal.
- It can help the TS compiler to recognize constant types better
Examples
let foo = 'foo'; // string
const bar= 'bar'; // 'bar'
const notConstant: const string = foo; // error
const trulyConstant: const string = bar; // success
type Baz = { baz: string } ;
const baz1: const Baz = { baz: 'baz' }; // success
const baz2: Baz = { baz: 'baz' }; // not a constant
const baz3: Baz = { baz: 'baz' } as const; // current available, same as baz1
type ConstArray<T> = const Array<T>;
// same as
type AnotherCosntArray<T> = Array<const T>;
function tuple<T extends ConstArray>(...args: T) {
return args;
}
const t1 = tuple(foo); // error!
const t2 = tuple(bar); // success! typeof t2 should be ['bar']
const t3 = tuple('a', 1, true, ['1'], baz1) // ['a', 1, true, ['1'], { baz: 'baz' }];
const t4 = tuple(baz2) // error!
let variable: const string = 'variable'; // this is possible
variable = 'another variable'; // success
variable = document.title; // error
let reactiveObject: const { foo: string } = { foo: 'foo' }; // success;
reactiveObject.foo = 'bar' // success
In fact, const only affects the assignment behavior, and a const type is considered to be the same as a non const type when read it.
A const type doesn't mean immutable, just means every primitive value should be literal, so maybe should call it literal type.
Alternatives
- Only add
extends const syntax, and add global generic type Constant<T = any> = T extends const ? T : never;
- Use
finally or static or literal(not exist in current keywords) keyword instead of const
- Use
type Literal instead of type Constant.
Related issues
#30680 Suggestion: Const contexts for generic type inference
Checklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
Reacted by Michael Borislav, Harry Solovay, NN, Guillaume Esquevin, Remi Rousselet, whzx5byb, Rafael Marques, Malcolm Still, jp2masa, Ilya Radchenko and 8 more ActivityMartinJohns commented on Oct 15, 2020
Sometimes we want to constrain a type or a parameter to be literal.
Can you provide a real world example where this would be useful? woshiguabi commented on Oct 15, 2020
Sometimes we want to constrain a type or a parameter to be literal.
Can you provide a real world example where this would be useful?
function tuple<T extends Array<string | number>>(...args: T) {
return args;
}
const t1 = tuple('a', 'b'); // ['a', 'b']
let bar = 'bar';
const t2 = tuple('foo', bar); // ['foo', string]
For intellisense, parameter must be literal. Constantiner commented on Oct 15, 2020 Why not to use just:
function tuple<T extends Array<string | number>>(...args: T) {
return args;
}
const t1 = tuple('a', 'b'); // ['a', 'b']
let bar = 'bar' as const;
const t2 = tuple('foo', bar); // ['foo', 'bar']
? woshiguabi commented on Oct 15, 2020
Why not to use just:
function tuple<T extends Array<string | number>>(...args: T) {
return args;
}
const t1 = tuple('a', 'b'); // ['a', 'b']
let bar = 'bar' as const;
const t2 = tuple('foo', bar); // ['foo', 'bar']
?
tuple may come from package.
The main goal is to constrain function parameters must be literal.
Now we have no way to diff const type and variable type.
MartinJohns commented on Oct 15, 2020
The main goal is to constrain function parameters must be literal.
But you didn't provide an example where this would be useful. Reacted by Ryan Cavanaugh tjjfvi commented on Oct 22, 2020 This could be accomplished with the conditionally assignable types discussed in #40779.
type StrictSubtype<T> = match U extends T => T extends U ? false : true;
type ConstString = StrictSubtype<string>; harrysolovay commented on Oct 22, 2020 The main use case––seems to be––to help library developers ensure that consumers are passing narrowed objects:
// BAD
acceptsNarrowed({
a: "A",
b: true,
c: 3,
});
// GOOD
acceptsNarrowed({
a: "A",
b: true,
c: 3,
} as const);
This feature would really shine! I've run into this problem many times. The following would be incredibly useful to me personally.
declare function acceptsNarrowed<O extends Constant<Record<PropertyKey, any>>>(o: O): void;
// or
declare function acceptsNarrowed<O extends const Record<PropertyKey, any>>(o: O): void; Reacted by Slick Kilmister, Michael Borislav, Martin Heidegger, Remi Rousselet, Maxim Kot, Philipp Uhl and BrodBlox09 KilianKilmister commented on Oct 25, 2020
The main goal is to constrain function parameters must be literal.
But you didn't provide an example where this would be useful.
I have to agree with Harry Solovay (@harrysolovay). I run into problems around this regularly, usually with some complex type-converting breaking down when it resieves non-literal string or number (especially when using the upcoming TempateString-literal type).
Also, i think this could solve many of the issues related to the notoriously strange symbol type. just to mention a few:
- precise symbol being returned from a function
- specifying a precise symbol in function parameters
- dynamic object keying with symbols
- symbol indexing
This would make it possible to give symbols to an API that can be referenced at a later point, both internally and by the API consumer.
// has the incredibly useless returntype of `{[x: string]: string }`
function someApi <T extends symbol>(someSymbol: T) {
return {[someSymbol]: 'dynamic symbol key'}
}
// return type is `symbol`, making it impossible to reference it again in the typeSystem.
function GetDynamicallyUniqueSymbol () {
const sym = Symbol('unique')
return sym
}
woshiguabi commented on Oct 25, 2020 After a few thinking, I think there are still some unresolved problems in the proposal.
Object and Array type probably shouldn't extends const to every member, because of prototype.
const foo = (arr: Constant<Array<string>>) => arr;
If object member extends const, Array.prototype.length will be const, so this function cannot be called.
Use this instead.
const bar = (arr: Array<Constant<string>>) => arr;
What about funciton type? Seems function type should not be affected by const.
We need more discussion.
This could be accomplished with the conditionally assignable types discussed in #40779.
type StrictSubtype<T> = match U extends T => T extends U ? false : true;
type ConstString = StrictSubtype<string>;
T6 (@tjjfvi) That will be a greet solution! I think generic is much better than a new syntax. KilianKilmister commented on Oct 26, 2020 woshiguabi
That will be a greet solution! I think generic is much better than a new syntax.
I have to be nitpickey here, but this is also new syntax. I also don't think this would address the issue in this thread. They do have overlap tho.
const foo = (arr: Constant<Array<string>>) => arr;
If object member extends const, Array.prototype.length will be const, so this function cannot be called.
I don't see a reason why this couldn't be called. I would interpret that as: the function only takes arrays with a known length (so eg. no string[]), but the specific length doesn't matter.
But it's good that you're brining these thing up.
What about funciton type? Seems function type should not be affected by const.
I have now idea how const should behave on a function directly. Maybe make everything that can be a litteral require a literal? But i don't think that would be a good idea as people could do the same with the individual params/return types. I don't think it should be usable on a function type, but it should stay similarly limited like a as const assertion.
There has been a want for something like this (or like the other proposal) for quite some time. There was a suggestion about extending the unique type to handle most of these things. I can't find the issue which suggested it right now, but it was quite popular and if i remember correctly, there were steps being taken to implement it.I think it was ultimatly shafted for some newly discovered technical reason.
If anybody knows which issue i'm talking about and happens to find it, it could potentially add quite a bit to these discussions. Might also be worth evaluating wether it could be implemented now, because it was discussed in length it could save us a lot of effort as would have to discuss very similar thing eventually woshiguabi commented on Oct 28, 2020 Andarist commented on Apr 11, 2021 Ryan Cavanaugh (@RyanCavanaugh) what's the process for adding this to the language by the external contributor? is this proposal non-destructive enough that a PR could be made and there would be a high chance of merging it after refining the details during the code review process? malcolmstill commented on Aug 17, 2022 I'm trying to do something that I believe the above suggestion would allow.
Let's say I want to use https://github.com/vultix/ts-results, but I'd like to:
- restrict all errors in my program to be a particular shape
- get exhaustive checking of an error type with a
kind or type field
If I wrapts-results Err function so that it takes <E extends string> (giving a new MyErr type say), I can do:
function double(a: number) {
if (a < 0.0) {
return Err({type: 'ERROR_LESS_THAN_ZERO' as const});
} else if (a > 100.0) {
return Err({type: 'ERROR_MORE_THAN_ONE_HUNDRED' as const});
}
return Ok(a * 2.0);
}
...and I am able to get completion and exhaustive checking of those type tags. I.e. the return type inference gives:
function double(a: number): MyErr<{
type: "ERROR_LESS_THAN_ZERO";
}> | MyErr<{
type: "ERROR_MORE_THAN_ONE_HUNDRED";
}> | TSResults.Ok<number>
But the problem is if I forget to add an as const on any of my errors I get this:
function double(a: number): MyErr<{
type: string;
}> | TSResults.Ok<number>
...and I've lost the discriminated union of type tags.
I'd love some way to say <E extends const string> (instead of <E extends string>) and so be forced to add as const on the type: field (or even better, not need to add the as const at all) tjjfvi commented on Aug 17, 2022 Malcolm Still (@malcolmstill) You can use the Narrow utility type for that:
type Narrow<T> =
| (T extends infer U ? U : never)
| Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
| ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> })
declare function Err<E extends { type: string }>(error: Narrow<E>): E;
const e = Err({ type: 'ERROR_LESS_THAN_ZERO' }) // no `as const` needed
// ^? - { type: "ERROR_LESS_THAN_ZERO" }malcolmstill commented on Aug 18, 2022 T6 (@tjjfvi) thanks, as you say Narrow lets me get rid of the as const, but let me clarify why that isn't sufficient for what I'm trying to do. If I write double in a different way:
function double(a: number, someString: string) {
if (a < 0.0) {
return Err({type: 'ERROR_LESS_THAN_ZERO' as const});
} else if (a > 100.0) {
return Err({type: 'ERROR_MORE_THAN_ONE_HUNDRED' as const});
} else if (a > 50.0) {
return Err({type: someString}); // I would like this to be a compile error because I want to enforce that type is const
}
return Ok(a * 2.0);
}
The above compiles, but collapses the return type to:
function double(a: number): MyErr<{
type: string;
}> | TSResults.Ok<number>
...I want it instead to not compile, and force me to make type: const knownasilya commented on Sep 24, 2022 Flutterish commented on Nov 25, 2022 I have a use case for this where I provide a const literal as a function parameter which decodes data into the format which the literal specifies - for example decode( someDataView, { someData: 'u16', otherData: 'string' } as const ). It gives really weird errors when the format is not const. zen0wu commented on Dec 4, 2022 (Centralizing the conversation from #51745)
I want to define an API for users to specify a fixed path in an object, consider
type Obj = {
a: {
b: {
c: "123"
}
}
}
type Get<T, K> = K extends keyof T ? NonNullable<T[K]> : never
type GetPath<T, P> = P extends PathKey[]
? P extends []
? T // If P is empty, return T.
: P extends [infer K, ...infer Rest]
? GetPath<Get<T, K>, Rest> // Else, recurse.
: never
: never
function set<T, P extends string[]>(obj: T, path: P, value: GetPath<T, P>) {}
set(
obj,
["a", "b", "c"],
value
)
Currently, typescript would infer this set call's P as string[], hence value will be inferred as never.
This however, works when P extends string, so one way of doing this would be
function set<T, P extends string>(obj: T, path: [P], value: unknown) {}
function set<T, P1 extends string, P2 extends string>(obj: T, path: [P1, P2], value: unknown) {}
function set<T, P1 extends string, P2 extends string, P3 extends string>(obj: T, path: [P1, P2, P3], value: unknown) {}
// ...
This feels both cumbersome and not scalable.
With the proposed change, it'll look like
funciton set<T, P extends const string[]>(obj: T, path: P, value: GetPath<T, P> {}
On the other hand, sometimes people do want their generics to be inferred as string, not the literal passed in.
Allowing a const bound would allow developers to express precisely what they want. Andarist commented on Dec 6, 2022 Zeno Wu (@zen0wu) This one actually can be done quite easily even today:
TS playground
The only drawback to this is that I have no idea how to make autocomplete work for this - without computing all possible paths eagerly. zen0wu commented on Dec 6, 2022 Mateusz Burzyński (@Andarist) good to know, thank you!
just so that i understand correctly, doing [...P] kind of enforces TS to infer it as a tuple type? is that a recommended best practice? Andarist commented on Dec 6, 2022
just so that i understand correctly, doing [...P] kind of enforces TS to infer it as a tuple type?
Yes
is that a recommended best practice?
Not sure. It's what I do though :P it seems to me like a way better approach than the older Tuple constraint workaround (type Tuple<T> = T[] | [T]) zen0wu commented on Dec 7, 2022
just so that i understand correctly, doing [...P] kind of enforces TS to infer it as a tuple type?
Yes
is that a recommended best practice?
Not sure. It's what I do though :P it seems to me like a way better approach than the older Tuple constraint workaround (type Tuple<T> = T[] | [T])
Mateusz Burzyński (@Andarist) Actually I tried this out. It has this weird behavior where.. it works when the definition of the variable and the type is in the same file, but when they're in different files, this breaks (TS falls back to [string] on P), I don't understand why... Andarist commented on Dec 8, 2022 Not sure if I understand - this only impacts how T might be inferred. If your variable is in a different file then it doesn't participate in the inference on the call site.
If you'd like to ensure that you can't pass any values that are not literals etc - then this might get more tricky. You can try to use this remapping technique with the presented Validate:
TS playground
In more complex scenarios it might be tricky to actually validate your things this way but usually you can get good results
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|