Repository navigation
Suggestion: Const contexts for generic type inference #30680
Description
Search Terms
const contexts; literals; narrowing; generics;
Suggestion
Allow const contexts for generic type parameter inference.
Often I've written and seen generic functions where the type parameter inference is intended to be as narrow as possible. Accomplishing this involves several "tricks" with generic constraints. It is more reminiscent of alchemy than I would like, and places quite a burden on the function signature and anyone with the misfortune of having to read and understand it:
// 🧙 what sorcery is this?? 🧙
type Narrowable = string | number | boolean | symbol | object | undefined | void | null | {};
declare function foo<N extends Narrowable, T extends { [k: string]: N | T | [] }>(x: T): T;
foo({ a: 1, b: "c", d: ["e", 2, true, { f: "g" }] });
// T inferred as { a: 1; b: "c"; d: ["e", 2, true, { f: "g"; }]; }Const contexts (#29510) are exactly the knob we want to turn here, and the "as const" syntax
is succinct, understandable, and non-mind-bending. Unfortunately, the only way to use this in
generic functions is from the caller's side, which is hard to guarantee:
declare function bar<T extends object>(x: T): T;
bar({ a: 1, b: "c", d: ["e", 2, true, { f: "g" }] } as const); // burden on function caller
// T inferred as {
// readonly a: 1; readonly b: "c"; d: readonly ["e", 2, true, { readonly f: "g"; }];
// }
bar({ a: 1, b: "c", d: ["e", 2, true, { f: "g" }] }); // oops!!
// T inferred as {
// a: number; b: string; d: (string | number | boolean | { f: string; })[];
// } 😢The suggestion here is to get the best of both worlds by allowing a const context to be specified in the generic type parameter declaration:
declare function baz<T extends const object>(x: T): T; // 🤔
declare function baz<T extends object as const>(x: T): T; // 🤷
declare function baz<const T extends object>(x: T): T; // 😵
declare function baz<const T const extends readonly object as const>(x: T): T; // 🧠💥🤪
// also possible, see #46937:
declare function baz<T extends object>(x: const T): T;
baz({ a: 1, b: "c", d: ["e", 2, true, { f: "g" }] });
// T inferred as {
// readonly a: 1; readonly b: "c"; d: readonly ["e", 2, true, { readonly f: "g"; }];
// } ❤🎉Related issues
#29510: const contexts
#10676: generics infer literals "T extends string | number | boolean"
#27179: generics infer tuples "T extends U[] | [U]"
#13347: probably can't make generics infer readonly
#16896: please narrow all object literals as much as possible
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.
EDIT: mentioning slightly different approach from #46937 where modifier goes on the type parameter instead of its constraint. It's not obvious to me if one has an advantage over the other.
Activity
aleclarson commentedon Apr 5, 2019
This would allow forcing a generic parameter to be inferred as a tuple instead of an array. 👍
sberan commentedon May 4, 2019
This would be incredible. I have an API for json schema which currently requires the caller to type
as consteverywhere:jcalz commentedon Dec 22, 2019
Relevant SO question: Dynamically generate return type based on array parameter of objects in TypeScript
My current answer involves adding a dummy type parameter
S extends stringto get string literal narrowing to happen in a different type parameterT extends {fieldName: S}; it reminded me that it would be so nice to have some more transparent syntax likeT extends {fieldName: const string}(or something) here.arciisine commentedon Dec 22, 2019
Closed #35821 in lieu of this. To expand upon this issue, I would also like to have support for this in Javascript, as
as constis not applicable outside of.tsfiles.dirkluijk commentedon Jan 23, 2020
Joe Calzaretta (@jcalz) Thank you for the
Narrowableworkaround.I need this as well, to infer string literals or enum values in generics.
jwulf commentedon Sep 30, 2020
Another relevant SO question: https://stackoverflow.com/questions/64056538/type-signature-that-infers-all-values
cyrilletuzi commentedon Oct 15, 2020
What is the status of this? I have the same issue as Sam Beran (@sberan): inferring the type from a JSON Schema would currently require
as consteverywhere on the caller side:Fails on function calling parameter with error:
While the following work:
Meaning the caller must do
as consteverywhere, which is not a reasonable option.millsp commentedon Dec 30, 2020
Hi all,
I think that I have finally solved this puzzle. We can now control const contexts on the behalf of the user. There we go:
Play it
This will land soon in https://github.com/millsp/ts-toolbelt
KisaragiEffective commentedon Jun 1, 2021
maybe make
Narrowed<T>isintrinsicand implement intsc?If so, the imaginary -
function x<T>(a: Narrowed<T>): foocan be implemented well.jcalz commentedon Jun 8, 2021
Another SO question: Converting values in a dictionary to literal values
leoblum commentedon Jun 25, 2021
+1
jcalz commentedon Nov 28, 2021
Edited to reflect slightly different approach as mentioned in #46937
captain-yossarian commentedon Feb 5, 2022
It would be great and make TS less verbose
shigma commentedon Apr 6, 2022
#48240 provides an excellent example of type annotations. now I would prefer
<const T>.thus we can also have
in const Tandin out const Tconstraints.yujiosaka commentedon Jun 21, 2022
pierre (@millsp) 's solution solved most of my problems like a charm!
But I want to share one corner case to push this issue forward, which is providing a library to anonymous people.
Although the
Narrowtype works when you pass the argument directly to functions,it does not work, of course, when the argument is defined beforehand without const assertion.
Playground
If it's an internal project, you can ask your teammates to always pass arguments directly.
It's certainly much easier than forcing them to pass
as constin every function call.But when you are providing a library, people may not follow your advice always.
What I expect with this proposal is that following code fails to compile because passed argument can be modified (thus the types cannot be narrowed).
It should accept const argument.
Also it would be nice if it automatically asserts argument as const when it's passed directly to the function (as originally proposed in this issue).
ExE-Boss commentedon Jul 5, 2022
See also:
readonlytype operator to allow correct definition ofObject.freeze#10725stephenh commentedon Nov 16, 2022
Fwiw I'm upgrading my project to TypeScript 4.9 and pierre (@millsp) 's
Constsolution is no longer working for me...I had been using a pattern like:
Where the 2nd param to
em.loadwould stay aconstby using theConsttype:Which would get
a1inferred asLoaded<Author, { publisher: {}, books: ... }>, which allowed thea1.publisher.getcall to be legal.But now with TS 4.9.3,
a1is instead getting inferred asLoaded<Author, LoadHint<Author>>, which makes the.getcall break.I can fix/force it by adding
as constto call sites:Which is going to be really verbose.
As anyone noticed
Constnot working for them on TS 4.9, and if so, any working fixes?Thanks!
...
Huh, I'm surprised, the 1st "poke something simple and see what happens" got it by to working for my specific use cases by removing
voidandnullfrom theNarrowable:What seems odd is I have to remove both
voidandnull; if I leave either, then it goes back to not working. ...so, that's good for me I guess, b/c I don't needvoidornullin the load hints that I'mConst-izing. 🤔 🤷Andarist commentedon Nov 16, 2022
It would be best if you could share a full inspectable TS playground with a report like this. It's hard to know what exactly might have changed if you don't share a repro case.
stephenh commentedon Nov 16, 2022
Ah hey Mateusz Burzyński (@Andarist) ! Yeah, that's a very fair ask; I was admittedly being lazy and hoping someone would have already hit an error and had a new/updated snippet.
I'll work on a playground-able repro of my issue.