TypeScript
TypeScript copied to clipboard
Feature Request: Expose TS configuration and internals as Types
Suggestion
🔍 Search Terms
expose CompilerOptions types
✅ Viability Checklist
My suggestion meets these guidelines:
- [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
- [x] This wouldn't change the runtime behavior of existing JavaScript code
- [x] This could be implemented without emitting different JS based on the types of the expressions
- [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- [x] This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
I want to suggest that TypeScript would add some global namespace that contains information about the current running setup:
- TS version (having some
AtLeast
type like I showcase in our "hacked use cases" below would be great too, but if we know the major and the minor we can also do that in userland) - active
compilerOptions
flags - it would be amazing to have some string unionTypeScript.CompilerOptions.ActiveFlags
where we could just do
'strictNullChecks' extends TypeScript.CompilerOptions.ActiveFlags ? SomeBehaviour : SomeLessSafeFallback
- the current
compilerOptions
in general - I don't know why someone would need access toTypeScript.CompilerOptions.allowSyntheticDefaultImports
but it would feel like a waste not to expose that
📃 Motivating Example
type MessageFromTypeScript = AtLeast<[TypeScript.Version.Major, TypeScript.Version.Minor], [4, 8]> extends true
? "it's the future"
: "you live in the past"
class MyRecord<T>{
get(index: string):
'noUncheckedIndexedAccess' extends TypeScript.CompilerOptions.ActiveFlags
? (T | undefined)
: T
}
💻 Use Cases
As library authors, we currently have to apply quite a number of hacks to support as many versions of TypeScript and as many different user configurations as possible.
Some example problems we are facing:
- Slight behavioral changes between TS versions. For example, #49307 forced us to change a
{}
toACR[T]
- but only in TS versions above 4.8. The fix breaks in older versions. - Behavioral changes depending on
tsconfig.json
settings. Imagine a mapped type with functions. The user specifiessomeMethod(action: PayloadAction<string>): void
and we map that over to asomeMethod(arg: string): void
. In the case the user specifiessomeMethod(action: PayloadAction<string | undefined>): void
, we map it over to an optional argument in the formsomeMethod(arg?: string): void
Now assume the user hasstrictNullChecks: false
in their tsconfig. Suddenly everything falls into the second case. - A library might for example also behave differently if the user has
noUncheckedIndexedAccess: true
in theirtsconfig.json
This leads to quite a few weird hacks in library types that might essentially break with every new release.
- Could be solved with a
typesVersion
, but honestly: if we can avoid having two complete sets of types, we want to avoid that. We had it for a while for pre-4.1 types and post-4.1 types and maintaining it was a pain. Right now, I've published https://github.com/phryneas/ts-version/ which uses a monstrouspackage.json
with typesVersions just to export the current TS Major and Minor.
So we solve our problem with an additional dependency and
import('@phryneas/ts-version').TSVersion.AtLeast<4, 8> extends true
? ACR[T]
: {}
In the past we used hacks like
export type AtLeastTS35<True, False> = [True, False][IsUnknown<ReturnType<<T>() => T>, 0, 1>]
for that.
For a while we also had a subfolder with a package.json
that would only use typesVersions
on one import in that subfolder. At least we did not have to maintain two complete separate type definitions, but let's say it was not a great developer experience.
2. We check for strictNullChecks
with
type WithStrictNullChecks<True, False> = undefined extends boolean ? False : True;
- We are not doing this yet but I'm sure we'd find a way to somehow detect
noUncheckedIndexedAccess
as well.
All that said: we have problems and we have solutions. But our solutions feel horrible and wrong. And also, having a package around with 120 different typesVersions
entries just to detect the current TypeScript version can't really be good for performance.
I hope you're going to give this at least some consideration :)
I've pitched something like this internally a few times and gotten a lukewarm reception. I think the key is figuring out some way to really scope this down to something that isn't likely to paint us into a corner in the future.
For example, today noUncheckedIndexAccess
(to pick something at random) is a boolean - might this eventually be a tri-state (true
, false
, "array"
), or something else more complex to represent different behavior for string vs numeric lookups? This is a feature request we've gotten. If you wrote something like
type Foo = {
true: something,
false: somethingElse
}[CompilerOptions['noUncheckedIndexAccess']
then we don't have any way to add new options without breaking this code -- but breaking less code in forward versioning is one of the main goals of this proposal in the first place.
type Foo = { true: something, false: somethingElse }[CompilerOptions['noUncheckedIndexAccess']
This specific example wouldn't work anyway since TS doesn't let you index a type with the true
or false
type, but the feature is niche enough that hopefully the few that would need to use it would be able to avoid forwards-compatibility issues. I'm generally in favor of the idea as I've had more than a few cases where I've needed to produce a type that has minute differences depending on strictNullChecks
mode so that it works correctly.
I'm curious whether it makes sense to expose all compiler options, or a very limited subset.
I'm also curious how best to handle something akin to AtLeast
. Internally we have the capability to match Semver ranges for use with "typesVersions"
, but introducing a type for that could potentially be abused in the type system to perform relational comparisons between literal types. I'd much rather have actual relational comparison types than have someone depend on a hack using semver comparisons with likely much worse performance.
I'm also curious how best to handle something akin to
AtLeast
. Internally we have the capability to match Semver ranges for use with"typesVersions"
, but introducing a type for that could potentially be abused in the type system to perform relational comparisons between literal types. I'd much rather have actual relational comparison types than have someone depend on a hack using semver comparisons with likely much worse performance.
Currently my userspace implementation looks like this:
https://github.com/phryneas/ts-version/blob/e3517aac4011d1649cc2782628659653ec60b30b/index.d.ts#L34-L42
Tbh, I'd be perfectly fine with leaving this as an user space implementation. It doesn't satisfy all of Semver, it just works for major-minor-combinations that are likely to ever be used by TypeScript - and once TS ever ships a 4.10 or a 10.0, I'll have to adjust it a bit. The painful-in-userland part is to have the Major
and Minor
version of the currently running TypeScript release.
For example, today
noUncheckedIndexAccess
(to pick something at random) is a boolean - might this eventually be a tri-state (true
,false
,"array"
), or something else more complex to represent different behavior for string vs numeric lookups? This is a feature request we've gotten. If you wrote something like
I think the feature I suggest here is niche enough to be used only in very specific use cases, and probably only ever in libraries.
From that perspective: We are library authors, we have to stay up to date anyways and we are used to this dance.
We run the library types with tests against 5 different TS versions to assure best compatibility and if something breaks, we'll hopefully notice soon enough and fix it as soon as possible. (I mean, all of this was only triggered because 4.8 is breaking another one of our types and we were lucky enough to notice it early)
No code lasts forever. Having something like this - even if it might break some day - is already 1000 times better than an ugly hack.
Something like this could for example be prefixed with _unstable
, _hereBeDragons
or _useOnYourOwnRisk
, it would still be absolute gold.
Something like this could for example be prefixed with
_unstable
,_hereBeDragons
or_useOnYourOwnRisk
, it would still be absolute gold.
Or "experimental", like the decorator support.
Hate to do the "+1" routine, but I'm in another situation where this would be extremely useful.
In https://github.com/microsoft/TypeScript/pull/50831#issuecomment-1253830522 , a change in TS 4.9 broke Reselect (boo!), and specifically a MergeParameters
type that took weeks to actually develop correctly initially . After some discussion, Anders provided me with an alternate implementation of that MergeParameters
type. The good news is it works. The bad news is that it requires TS 4.7+, and now I have to figure out how to ship this to our users.
We already ship one entire other set of typedefs - the old ginormous 15-overloads-per-function typedefs that were in Reselect up through 4.0, before we completely rewrote our types in 4.1 to add do a much better job of inference (which is when we added this MergeParameters
type).
But, 4.7 is far too new for us to require as a baseline, so I've got to ship a package that works with, realistically, 4.2 and above.
So how do I get users who are on 4.2-4.6 to use the existing MergeParameters
implementation, and users on 4.7+ to use the new implementation, without shipping another duplicate set of typedefs?
Well, as Lenz pointed out above, you can pull some stupid hacks with build setup:
- Create a folder and put two different files with different impls of the same type inside
- Add an index file and re-export one of those
- Add a file named
package.dist.json
containingtypesVersions
that points to both of those.d.ts
files - Copy that
package.dist.json
over todist/some/package.json
during the actual build/publish step
That way during dev TS just imports the type as normal, but the built version sees that some/package.json
, sees typesVersions
, finds the right .d.ts
file, and imports the right implementation for itself.
This is, frankly, some ridiculous shenanigans :)
If there was some way to declare in the code itself "hey, if this is TS 4.7 or greater use implementation A, else use implementation B", this would be much simpler.
I feel like there are two things being discussed here - they are highly related but at the same time risks associated with both of them are very different.
It feels that there is a reasonable pushback to exposing compiler options (although I also think that there are reasons for libraries to access this kind of information). This one is probably so bikesheddable that it won't happen any time soon (let's continue discussing this though!).
On the other hand, exposing a TS version somehow is way less controversial (that doesn't mean it's not controversial at all 😉 ). How likely this one is to happen? If this feature request would get approved then I could take a stab at implementing this.
That being said - this won't really help @markerikson right now as this wouldn't be backported to older TS versions. It could make it easier to select the implementation of the types in the future though. For the time being... I don't feel like @phryneas/ts-version
is that bad of an idea.
If there was some way to declare in the code itself "hey, if this is TS 4.7 or greater use implementation A, else use implementation B", this would be much simpler.
I think at that point we will end up with some kind of templating engine that creates multiple different typesVersions
- the changes I ask for here will only allow to work with stuff that does not introduce parsing errors.
Nonetheless, it's something that would help a lot - is there any progress or decision on this?
It feels that there is a reasonable pushback to exposing compiler options (although I also think that there are reasons for libraries to access this kind of information). This one is probably so bikesheddable that it won't happen any time soon (let's continue discussing this though!).
Yeah, please let's continue discussing this and not let the discussion die here!
Everything we can come up with here will be a 100 times less prone to breakage than trying to detect enabled features with whacky TS types in code.
Adding my support for this - would be great for library authors and users.
In the meantime, I've published a tiny package called uncheckedindexed
, which uses a workaround to detect whether the user is using noUncheckedIndexedAccess
or not - which to me seems like the most common use case for this kind of feature.
Would love for this to become a supported feature by Typescript, as honestly the package workaround is a little gross 😄