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

More specific inference for constrained 'infer' types in template literal types #48094

Merged
merged 10 commits into from
May 27, 2022

Conversation

rbuckton
Copy link
Member

@rbuckton rbuckton commented Mar 2, 2022

This modifies inference in template literal types when inferring to a constrained type variable by leveraging the constraint of the type variable to infer a more specific type:

// helper that enforces a constraint on an `infer T` type
type Is<T extends U, U> = T;

// today
type T0 = "100" extends `${Is<infer T, number>}` ? T : never; // number
type T1 = "100" extends `${Is<infer T, bigint>}` ? T : never; // bigint
type T2 = "true" extends `${Is<infer T, boolean>}` ? T : never; // boolean

// after this change
type T0 = "100" extends `${Is<infer T, number>}` ? T : never; // 100
type T1 = "100" extends `${Is<infer T, bigint>}` ? T : never; // 100n
type T2 = "true" extends `${Is<infer T, boolean>}` ? T : never; // true

The primary motivation for this feature is converting numeric string index keys from a tuple type into their numeric literal equivalents. While you can use something like a mapping table for a subset of numbers (i.e., as many as you are willing to manually define), that can run into issues when that might then generate a too-large union. Rather than introducing a feature specific to numeric string index keys, this instead focuses on a more general purpose solution that can apply to any valid template literal type placeholder (i.e., string, number, bigint, boolean, null, and undefined).

Please note that this approach cannot be used to convert an arbitrary numeric string into a number or bigint literal type, as the inferred type must still satisfy the extends condition in the conditional type. This means that a string such as "0x10" cannot be converted to a number, since that value would not round-trip: If you attempt to infer a number literal type from "0x10", the result would be 16, however when 16 is substituted back into the string literal type it becomes "16", which is not a supertype of "0x10".

Also note that this PR does not introduce any new mechanism for establishing a constraint for an infer T type. Instead, a helper type like Is (above) might be necessary to enforce the constraint. A syntactic mechanism to constrain infer T would be valuable, but is out of scope for this PR.

Addendum: If we decide to take #48112, the above example could be rewritten using infer T extends ... instead of the Is type alias:

type T0 = "100" extends `${infer T extends number}` ? T : never; // 100
type T1 = "100" extends `${infer T extends bigint}` ? T : never; // 100n
type T2 = "true" extends `${infer T extends boolean}` ? T : never; // true

Fixes #47141

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Mar 2, 2022
@sandersn sandersn added this to Not started in PR Backlog via automation Mar 10, 2022
@sandersn sandersn moved this from Not started to Waiting on reviewers in PR Backlog Mar 10, 2022
@treybrisbane
Copy link

The primary motivation for this feature is converting numeric string index keys from a tuple type into their numeric literal equivalents.

Could a new intrinsic like

type ParseInt<AString extends string, Radix extends number = 10> = intrinsic;

solve that problem?

The reason I ask is that a ParseInt intrinsic is something I've been considering proposing, but I haven't yet gotten around to doing a proper write-up for it. 😅

@rbuckton
Copy link
Member Author

Could a new intrinsic like [...] solve that problem?

I'd considered that approach, however the intrinsic type functionality is very strongly tied to string literal types currently and would require quite a bit of refactoring to change. Its something we are likely to do in the future, but I felt the general purpose approach above to be both adequate for numeric string index keys (which are values that are round-trippable), as well as more flexible for working with other placeholder types like bigint/boolean.

@sno2
Copy link
Contributor

sno2 commented Mar 12, 2022

Hello, thank you very much for your work. Would the following behavior be correct with this PR?

type NumToBigint<T extends number> = `${T}` extends `${Is<infer $BN, bigint>}` ? $BN : never;

type asdf = NumToBigint<23>; // 23n
type asdf2 = NumToBigint<23.5>; // never
type adsf5 = NumToBigint<-2>; // -2n

@rbuckton
Copy link
Member Author

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 14, 2022

Heya @rbuckton, I've started to run the tarball bundle task on this PR at ed2be49. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

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/121826/artifacts?artifactName=tgz&fileId=50F732BE8D5A25274F80CDE406275C2EBB6AA3559A8E66687CB9BBAFF38E6CF602&fileName=/typescript-4.7.0-insiders.20220314.tgz"
    }
}

and then running npm install.

@rbuckton
Copy link
Member Author

Hello, thank you very much for your work. Would the following behavior be correct with this PR?

type NumToBigint<T extends number> = `${T}` extends `${Is<infer $BN, bigint>}` ? $BN : never;

type asdf = NumToBigint<23>; // 23n
type asdf2 = NumToBigint<23.5>; // never
type adsf5 = NumToBigint<-2>; // -2n

Yes, that is correct.

@rbuckton rbuckton requested a review from sandersn March 18, 2022 23:41
src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
PR Backlog automation moved this from Waiting on reviewers to Waiting on author Mar 22, 2022
@ahejlsberg
Copy link
Member

Additionally, might be good to have some tests that check inference to constrained type parameters of functions. For example:

declare function test<T extends string | number>(s: `**${T}**`): T;
const x = test('**123**');  // Should produce "123" | 123

@rbuckton
Copy link
Member Author

Additionally, might be good to have some tests that check inference to constrained type parameters of functions. For example:

declare function test<T extends string | number>(s: `**${T}**`): T;
const x = test('**123**');  // Should produce "123" | 123

This doesn't seem to work. We correctly produce inference candidates of "123" and 123, but getCovariantInference returns only "123", which seems to be due to getCommonSupertype/getSupertypeOrUnion picking only the left-most literal type since the candidates don't share a common supertype.

@rbuckton
Copy link
Member Author

Ah, I may just need to add a new inference priority to indicate an inference to a template type placeholder.

src/compiler/types.ts Outdated Show resolved Hide resolved
@rbuckton
Copy link
Member Author

I've added a new InferencePriority for template type placeholders and included it in PriorityImpliesCombination so that we produce the union in getCovariantInference rather than just the first literal type in the union.

@rbuckton
Copy link
Member Author

@typescript-bot perf test
@typescript-bot run dt
@typescript-bot test this
@typescript-bot user test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 22, 2022

Heya @rbuckton, I've started to run the perf test suite on this PR at df23e95. You can monitor the build here.

Update: The results are in!

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 22, 2022

Heya @rbuckton, I've started to run the parallelized Definitely Typed test suite on this PR at df23e95. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 22, 2022

Heya @rbuckton, I've started to run the extended test suite on this PR at df23e95. You can monitor the build here.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Mar 22, 2022

Heya @rbuckton, I've started to run the parallelized community code test suite on this PR at df23e95. You can monitor the build here.

@rbuckton rbuckton force-pushed the templateLiteralTypeInferToNonString branch from 21c053c to 00c4b26 Compare May 10, 2022 17:56
@rbuckton
Copy link
Member Author

@ahejlsberg can you take another look?

@weswigham I put in a check that will hopefully work with #47050, or at least be a start.

@anuraghazra
Copy link

Will this also support floating point numbers? or just integers?

@somebody1234
Copy link

@reverofevil
Copy link

Oh, so we're not far from computing a sum of two numeric literal types!

type Test = Sum<1099123, 9901123>

type ToString<A extends number> = `${A}`
type Reverse<A extends string> =
    A extends `${infer H}${infer T}`
        ? `${Reverse<T>}${H}`
        : A
type HT<T> = T extends `${infer H}${infer T}` ? {H: H, T: T} : {H: '0', T: ''}
type Head<T> = T extends `${infer H}${any}` ? H : '0'
type Tail<T> = T extends `${string}${infer T}` ? T : ''
type R0 = Tail<''>
type Concat<A, B> = A extends string ? B extends string ? `${A}${B}` : never : never
type Nums = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
type Shuffle<T extends any[]> = T extends [infer H, ...infer T] ? [...T, H] : never
type MakeTable<T extends any[], S extends any[]> = T extends [any, ...infer T] ? [S, ...MakeTable<T, Shuffle<S>>] : []
type SumTable = MakeTable<Nums, Nums>
type SumD = {0: SumTable, 1: Shuffle<SumTable>}
type Index<B, A> = A extends keyof B ? B[A] : never
type SumDR<A, B, C> = Index<Index<Index<SumD, A>, B>, C>
type Carries = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
type CarryTable = MakeTable<[0, ...Nums], Carries>
type SumC = {0: CarryTable, 1: Shuffle<CarryTable>}
type Lookup<T, C, A, B> = Index<Index<Index<T, C>, Head<A>>, Head<B>>
type SumS<A extends string, B extends string, C = 0> = Concat<A, B> extends ''
    ? C extends 1 ? '1' : ''
    : Concat<SumS<Tail<A>, Tail<B>, Lookup<SumC, C, A, B>>, Lookup<SumD, C, A, B>>
type Sum<A extends number, B extends number> = SumS<Reverse<ToString<A>>, Reverse<ToString<B>>>

src/compiler/checker.ts Outdated Show resolved Hide resolved
src/compiler/checker.ts Outdated Show resolved Hide resolved
@rbuckton rbuckton force-pushed the templateLiteralTypeInferToNonString branch from 80877c2 to 9ccca98 Compare May 24, 2022 23:41
PR Backlog automation moved this from Waiting on author to Needs merge May 27, 2022
@rbuckton rbuckton merged commit ba45252 into main May 27, 2022
PR Backlog automation moved this from Needs merge to Done May 27, 2022
@rbuckton rbuckton deleted the templateLiteralTypeInferToNonString branch May 27, 2022 18:05
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
Archived in project
PR Backlog
  
Done
Development

Successfully merging this pull request may close these issues.

Feature request: Ability to parse a stringified number on type-level
9 participants