Add synthetic TypeScriptSettings interface that exposes some compiler options to type system
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:
interface TypeScriptSettings {
version: string; // e.g., "5.5.0-dev", etc.
versionMajorMinor: string; // e.g., "5.5", etc.
locale: string | undefined; // e.g., "en-US", etc.
target: string; // e.g., "es2023", etc.
module: string; // e.g., "node16", etc.
moduleResolution: string; // e.g., "node16", etc.
customConditions: readonly string[] | undefined;
exactOptionalPropertyTypes: boolean;
noImplicitAny: boolean;
noUncheckedIndexedAccess: boolean;
strictBindCallApply: boolean;
strictFunctionTypes: boolean;
strictNullChecks: boolean;
useDefineForClassFields: boolean;
}
Some compiler options can already be detected by the type system, such as --strictNullChecks or --exactOptionalPropertyTypes:
type StrictNullChecks = (1 | undefined extends 1 ? false : true);
type ExactOptionalPropertyTypes = { _?: true | undefined } extends { _?: true } ? false : true;
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 of Iterable, 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 than Function. 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 new Iterator constructor introduced by #58222.
Instead, we can leverage TypeScriptSettings to handle this case:
type BuiltinIteratorReturnType = TypeScriptSettings extends { noUncheckedIndexedAccess: true } ? undefined : any;
...
interface Array<T> {
value(): IterableIterator<T, BuiltinIteratorReturnType>;
}
Here, we can use the existing --noUncheckedIndexedAccess flag to control the strictness of the TReturn type passed to IterableIterator. When the flag is unset, we fallback to the current behavior for IterableIterator (where TReturn is any). When the flag is set, we instead pass undefined for TReturn.
Why use an interface and not an intrinsic type alias?
Rather than use intrinsic, the new TypeScriptSettings type is introduced as an interface to allow for declaration merging when a library needs to target both newer and older versions of TypeScript:
type __StrictNullAwareType<T> = ...;
type __NonStrictNullAwareType<T> = ...;
type __LegacyType<T> = ...;
export type NullAwareType<T> =
TypeScriptSettings extends { strictNullChecks: true } ? __StrictNullAwareType<T> :
TypeScriptSettings extends { strictNullChecks: false} ? __NonStrictNullAwareType<T> :
__LegacyType<T>;
// stub TypeScriptSettings to support older compilers:
declare global { interface TypeScriptSettings {} }
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 like customConditions have some interesting potential, but may end up being cut as they could be abused.
Fixes #50196 Related #58243
@typescript-bot: pack this
Starting jobs; this comment will be updated as builds start and complete.
| Command | Status | Results |
|---|---|---|
: pack this |
✅ Started | ✅ Results |
Hey @rbuckton, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:
{
"devDependencies": {
"typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/161592/artifacts?artifactName=tgz&fileId=D6D74C81D27AA9C95BA21E3472278D27CD883DAAEBE92EA3EF4DF165163C9E1D02&fileName=/typescript-5.5.0-insiders.20240501.tgz"
}
}
and then running npm install.
There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/[email protected]".;
This would address (at least partially) https://github.com/microsoft/TypeScript/issues/50196 , cc @phryneas
This is great! ❤️
This would address (at least partially) #50196 , cc @phryneas
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 TypeScriptSettings["version"]. It's possible to do with template literal types, but is by no means convenient:
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 --skipLibCheck, but could at least catch some incorrect usages of a library until such time as a more comprehensive mechanism for settings validation is adopted.
And here is a similar example to the previous one, utilizing TypeScriptSettings["locale"] to provide a localized error message:
// @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.