Skip to content
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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

rbuckton
Copy link
Member

@rbuckton rbuckton commented May 1, 2024

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;

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 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

@rbuckton
Copy link
Member Author

rbuckton commented May 1, 2024

@typescript-bot: pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented May 1, 2024

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
: pack this ✅ Started ✅ Results

@typescript-bot
Copy link
Collaborator

typescript-bot commented May 1, 2024

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]".;

@Andarist
Copy link
Contributor

Andarist commented May 2, 2024

This would address (at least partially) #50196 , cc @phryneas

@phryneas
Copy link

phryneas commented May 2, 2024

This is great! ❤️

@rbuckton
Copy link
Member Author

rbuckton commented May 2, 2024

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

TypeScript Playground

It's not likely that we would introduce a comparison mechanism like this as part of this PR, however.

@rbuckton
Copy link
Member Author

rbuckton commented May 2, 2024

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'.

TypeScript Playground

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.

@rbuckton
Copy link
Member Author

rbuckton commented May 2, 2024

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'.

TypeScript Playground

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature Request: Expose TS configuration and internals as Types
4 participants