Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Feature Request: Expose TS configuration and internals as Types #50196

Open
5 tasks done
phryneas opened this issue Aug 5, 2022 · 10 comments
Open
5 tasks done

Feature Request: Expose TS configuration and internals as Types #50196

phryneas opened this issue Aug 5, 2022 · 10 comments
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@phryneas
Copy link

phryneas commented Aug 5, 2022

Suggestion

πŸ” Search Terms

expose CompilerOptions types

βœ… Viability 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, new syntax sugar for JS, etc.)
  • 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 union TypeScript.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 to TypeScript.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:

  1. Slight behavioral changes between TS versions. For example, Nightly bug(?) with infering function generic while being part of intersectionΒ #49307 forced us to change a {} to ACR[T] - but only in TS versions above 4.8. The fix breaks in older versions.
  2. Behavioral changes depending on tsconfig.json settings. Imagine a mapped type with functions.
    The user specifies someMethod(action: PayloadAction<string>): void and we map that over to a someMethod(arg: string): void.
    In the case the user specifies someMethod(action: PayloadAction<string | undefined>): void, we map it over to an optional argument in the form someMethod(arg?: string): void
    Now assume the user has strictNullChecks: false in their tsconfig. Suddenly everything falls into the second case.
  3. A library might for example also behave differently if the user has noUncheckedIndexedAccess: true in their tsconfig.json

This leads to quite a few weird hacks in library types that might essentially break with every new release.

  1. 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 monstrous package.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;
  1. 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 :)

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript In Discussion Not yet reached consensus labels Aug 5, 2022
@RyanCavanaugh
Copy link
Member

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.

@rbuckton
Copy link
Member

rbuckton commented Aug 5, 2022

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.

@phryneas
Copy link
Author

phryneas commented Aug 5, 2022

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.

@phryneas
Copy link
Author

phryneas commented Aug 5, 2022

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.

@MartinJohns
Copy link
Contributor

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.

@markerikson
Copy link

Hate to do the "+1" routine, but I'm in another situation where this would be extremely useful.

In #50831 (comment) , 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 containing typesVersions that points to both of those .d.ts files
  • Copy that package.dist.json over to dist/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.

@Andarist
Copy link
Contributor

Andarist commented Nov 3, 2022

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.

@phryneas
Copy link
Author

phryneas commented Nov 3, 2022

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?

@phryneas
Copy link
Author

phryneas commented Nov 3, 2022

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.

@EskiMojo14
Copy link

EskiMojo14 commented May 8, 2023

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 πŸ˜„

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
7 participants