-
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
Add synthetic TypeScriptSettings interface that exposes some compiler options to type system #58396
Conversation
… flags to type system
@typescript-bot: pack this |
Hey @rbuckton, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
This is great! ❤️ |
Per @RyanCavanaugh's comment in #50196, the biggest issue with future extensibility would be how people choose to depend on a type like this. It may be that the best we can hope for is that developers would code defensively against future changes, such as: type Foo =
TypeScriptSettings extends { noUncheckedIndexAccess: false } ? something :
TypeScriptSettings extends { noUncheckedIndexAccess: true } ? somethingElse :
never; The other missing piece to #50196 is a way to conveniently perform range tests against import { type N, type A } from "ts-toolbelt";
type Eq<X extends number, Y extends number> = A.Is<X, Y, "equals">;
type VersionGreaterEq<A extends `${bigint}.${bigint}`, B extends `${bigint}.${bigint}`> =
A extends `${infer AMajor extends number}.${infer AMinor extends number}` ?
B extends `${infer BMajor extends number}.${infer BMinor extends number}` ?
[Eq<AMajor, BMajor>, N.GreaterEq<AMinor, BMinor>] extends [1, 1] ? true :
N.Greater<AMajor, BMajor> extends 1 ? true : false :
false :
false;
type TSVer = TypeScriptSettings["versionMajorMinor"];
// ^? type TSVer = "5.5"
type T1 = VersionGreaterEq<TSVer, "5.5">;
// ^? type T1 = true
type T2 = VersionGreaterEq<TSVer, "5.6">;
// ^? type T2 = false It's not likely that we would introduce a comparison mechanism like this as part of this PR, however. |
Another scenario I'd investigated recently was how a library could ensure that specific compiler options are set for it to be used correctly: // @exactOptionalPropertyTypes: false
interface TypeScriptSettingsError<Message extends string> { error: Message }
type ExpectedSetting<K extends keyof TypeScriptSettings, V, Message extends string> =
TypeScriptSettings extends { [P in K]: V } ? never :
TypeScriptSettingsError<Message>;
type CheckSetting<_T extends never> = never;
type _ = CheckSetting<ExpectedSetting<"exactOptionalPropertyTypes", true, `To use this package you must set 'exactOptionalPropertyTypes' to 'true' in your tsconfig.json.`>>;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
// error: Type 'TypeScriptSettingsError<"To use this package you must set 'exactOptionalPropertyTypes' to 'true' in your tsconfig.json.">' does not satisfy the constraint 'never'. Unfortunately, this check would defeated by |
And here is a similar example to the previous one, utilizing // @exactOptionalPropertyTypes: false
// @locale: fr-FR
interface TypeScriptSettingsError<Message extends string> { error: Message }
type ExpectedSetting<K extends keyof TypeScriptSettings, V, Message extends string> =
TypeScriptSettings extends { [P in K]: V } ? never :
TypeScriptSettingsError<Message>;
type CheckSetting<_T extends never> = never;
interface LocalizedMessages {
default: {
exactOptionalPropertyTypes: `To use this package you must set 'exactOptionalPropertyTypes' to 'true' in your tsconfig.json.`
},
"fr-FR": {
exactOptionalPropertyTypes: `Pour utiliser ce package, vous devez définir 'exactOptionalPropertyTypes' sur 'true' dans votre tsconfig.json.`
},
}
type Messages = {
[P in keyof LocalizedMessages["default"]]:
TypeScriptSettings extends { locale: infer Locale extends keyof LocalizedMessages } ?
P extends keyof LocalizedMessages[Locale] ?
LocalizedMessages[Locale][P] :
LocalizedMessages["default"][P] :
LocalizedMessages["default"][P];
};
type _ = CheckSetting<ExpectedSetting<"exactOptionalPropertyTypes", true, Messages["exactOptionalPropertyTypes"]>>;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~...
// error: Type 'TypeScriptSettingsError<"Pour utiliser ce package, vous devez définir 'exactOptionalPropertyTypes' sur 'true' dans votre tsconfig.json.">' does not satisfy the constraint 'never'. |
We've opted not to pursue this mechanism at this time. |
This adds a synthetic
TypeScriptSettings
interface to the global scope that reflects the state of current compilation's compiler options. The synthetic interface looks something like the following:Some compiler options can already be detected by the type system, such as
--strictNullChecks
or--exactOptionalPropertyTypes
:TypeScript playground
But most compiler options are not so easily derived.
Supporting stricter
Iterable
/IterableIterator
One of the key benefits of this approach is that types can be tailored for specific compiler options without the need to introduce new syntax or new
intrinsic
types. For example, we found that the changes in #58243, which introduces new type parameters for a stricter definition ofIterable
, caused far too many breaks were we to ship those changes unflagged. When we shipped--strictBindCallApply
, we were able to control strictness by swapping out the base type of a function or constructor type with something stricter thanFunction
. Doing the same for all of the different JS built-ins that are iterable becomes far tricker, and is more likely to run afoul of users writing custom iterators, or would cause complications with the newIterator
constructor introduced by #58222.Instead, we can leverage
TypeScriptSettings
to handle this case:Here, we can use the existing
--noUncheckedIndexedAccess
flag to control the strictness of theTReturn
type passed toIterableIterator
. When the flag is unset, we fallback to the current behavior forIterableIterator
(whereTReturn
isany
). When the flag is set, we instead passundefined
forTReturn
.Why use an interface and not an
intrinsic
type alias?Rather than use
intrinsic
, the newTypeScriptSettings
type is introduced as aninterface
to allow for declaration merging when a library needs to target both newer and older versions of TypeScript:TODO
While this PR is fully functional, we must still discuss which flags to include/exclude, as well as whether to continue with
TypeScriptSetings
or use a different name. I've only included options that could have an impact on types. Some options likecustomConditions
have some interesting potential, but may end up being cut as they could be abused.Fixes #50196
Related #58243