Skip to content

Commit 0758d84

Browse files
committed
More specific inference for constrained 'infer' types in template literal types
1 parent e2bd89b commit 0758d84

File tree

6 files changed

+1116
-6
lines changed

6 files changed

+1116
-6
lines changed

src/compiler/checker.ts

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22109,13 +22109,31 @@ namespace ts {
2210922109
sourceEnd.slice(sourceEnd.length - endLen) !== targetEnd.slice(targetEnd.length - endLen);
2211022110
}
2211122111

22112-
function isValidBigIntString(s: string): boolean {
22112+
/**
22113+
* Tests whether the provided string can be parsed as a number.
22114+
* @param s The string to test.
22115+
* @param roundTripOnly Indicates the resulting number matches the input when converted back to a string.
22116+
*/
22117+
function isValidNumberString(s: string, roundTripOnly: boolean): boolean {
22118+
if (s === "") return false;
22119+
const n = +s;
22120+
return isFinite(n) && (!roundTripOnly || "" + n === s);
22121+
}
22122+
22123+
/**
22124+
* Tests whether the provided string can be parsed as a bigint.
22125+
* @param s The string to test.
22126+
* @param roundTripOnly Indicates the resulting bigint matches the input when converted back to a string.
22127+
*/
22128+
function isValidBigIntString(s: string, roundTripOnly: boolean): boolean {
22129+
if (s === "") return false;
2211322130
const scanner = createScanner(ScriptTarget.ESNext, /*skipTrivia*/ false);
2211422131
let success = true;
2211522132
scanner.setOnError(() => success = false);
2211622133
scanner.setText(s + "n");
2211722134
let result = scanner.scan();
22118-
if (result === SyntaxKind.MinusToken) {
22135+
const negative = result === SyntaxKind.MinusToken;
22136+
if (negative) {
2211922137
result = scanner.scan();
2212022138
}
2212122139
const flags = scanner.getTokenFlags();
@@ -22124,7 +22142,8 @@ namespace ts {
2212422142
// * a bigint can be scanned, and that when it is scanned, it is
2212522143
// * the full length of the input string (so the scanner is one character beyond the augmented input length)
2212622144
// * it does not contain a numeric seperator (the `BigInt` constructor does not accept a numeric seperator in its input)
22127-
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator);
22145+
return success && result === SyntaxKind.BigIntLiteral && scanner.getTextPos() === (s.length + 1) && !(flags & TokenFlags.ContainsSeparator)
22146+
&& (!roundTripOnly || s === pseudoBigIntToString({ negative, base10Value: parsePseudoBigInt(scanner.getTokenValue()) }));
2212822147
}
2212922148

2213022149
function isValidTypeForTemplateLiteralPlaceholder(source: Type, target: Type): boolean {
@@ -22133,8 +22152,8 @@ namespace ts {
2213322152
}
2213422153
if (source.flags & TypeFlags.StringLiteral) {
2213522154
const value = (source as StringLiteralType).value;
22136-
return !!(target.flags & TypeFlags.Number && value !== "" && isFinite(+value) ||
22137-
target.flags & TypeFlags.BigInt && value !== "" && isValidBigIntString(value) ||
22155+
return !!(target.flags & TypeFlags.Number && isValidNumberString(value, /*roundTripOnly*/ false) ||
22156+
target.flags & TypeFlags.BigInt && isValidBigIntString(value, /*roundTripOnly*/ false) ||
2213822157
target.flags & (TypeFlags.BooleanLiteral | TypeFlags.Nullable) && value === (target as IntrinsicType).intrinsicName);
2213922158
}
2214022159
if (source.flags & TypeFlags.TemplateLiteral) {
@@ -22712,7 +22731,63 @@ namespace ts {
2271222731
// succeed. That would be a pointless and confusing outcome.
2271322732
if (matches || every(target.texts, s => s.length === 0)) {
2271422733
for (let i = 0; i < types.length; i++) {
22715-
inferFromTypes(matches ? matches[i] : neverType, types[i]);
22734+
const source = matches ? matches[i] : neverType;
22735+
const target = types[i];
22736+
22737+
// If we are inferring from a string literal type to a type variable whose constraint includes one of the
22738+
// allowed template literal placeholder types, infer from a literal type corresponding to the constraint.
22739+
let sourceTypes: Type[] | undefined;
22740+
if (source.flags & TypeFlags.StringLiteral && target.flags & TypeFlags.TypeVariable) {
22741+
const inferenceContext = getInferenceInfoForType(target);
22742+
const constraint = inferenceContext ? getConstraintOfTypeParameter(inferenceContext.typeParameter) : undefined;
22743+
if (inferenceContext && constraint) {
22744+
const str = (source as StringLiteralType).value;
22745+
const constraintTypes = constraint.flags & TypeFlags.Union ? (constraint as UnionType).types : [constraint];
22746+
for (const constraintType of constraintTypes) {
22747+
if (constraintType.flags & TypeFlags.StringLike) {
22748+
sourceTypes ??= [];
22749+
sourceTypes.push(source);
22750+
}
22751+
if (constraintType.flags & TypeFlags.NumberLike && isValidNumberString(str, /*roundTripOnly*/ true)) {
22752+
sourceTypes ??= [];
22753+
sourceTypes.push(getNumberLiteralType(+str));
22754+
}
22755+
if (constraintType.flags & TypeFlags.BigIntLike && isValidBigIntString(str, /*roundTripOnly*/ true)) {
22756+
const negative = str.startsWith("-");
22757+
const base10Value = parsePseudoBigInt(`${negative ? str.slice(1) : str}n`);
22758+
sourceTypes ??= [];
22759+
sourceTypes.push(getBigIntLiteralType({ negative, base10Value }));
22760+
}
22761+
if (constraintType.flags & TypeFlags.BooleanLike) {
22762+
if (str === trueType.intrinsicName) {
22763+
sourceTypes ??= [];
22764+
sourceTypes.push(trueType);
22765+
}
22766+
else if (str === falseType.intrinsicName) {
22767+
sourceTypes ??= [];
22768+
sourceTypes.push(falseType);
22769+
}
22770+
}
22771+
if (constraintType.flags & TypeFlags.Null && str === nullType.intrinsicName) {
22772+
sourceTypes ??= [];
22773+
sourceTypes.push(nullType);
22774+
}
22775+
if (constraintType.flags & TypeFlags.Undefined && str === undefinedType.intrinsicName) {
22776+
sourceTypes ??= [];
22777+
sourceTypes.push(undefinedType);
22778+
}
22779+
}
22780+
}
22781+
}
22782+
22783+
if (sourceTypes) {
22784+
for (const source of sourceTypes) {
22785+
inferFromTypes(source, target);
22786+
}
22787+
}
22788+
else {
22789+
inferFromTypes(source, target);
22790+
}
2271622791
}
2271722792
}
2271822793
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
tests/cases/conformance/types/literal/templateLiteralTypes4.ts(93,12): error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
2+
tests/cases/conformance/types/literal/templateLiteralTypes4.ts(97,12): error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
3+
4+
5+
==== tests/cases/conformance/types/literal/templateLiteralTypes4.ts (2 errors) ====
6+
type Is<T extends U, U> = T;
7+
8+
type T0 = "100" extends `${Is<infer N, number>}` ? N : never; // 100
9+
type T1 = "-100" extends `${Is<infer N, number>}` ? N : never; // -100
10+
type T2 = "1.1" extends `${Is<infer N, number>}` ? N : never; // 1.1
11+
type T3 = "8e-11" extends `${Is<infer N, number>}` ? N : never; // 8e-11 (0.00000000008)
12+
type T4 = "0x10" extends `${Is<infer N, number>}` ? N : never; // number (not round-trippable)
13+
type T5 = "0o10" extends `${Is<infer N, number>}` ? N : never; // number (not round-trippable)
14+
type T6 = "0b10" extends `${Is<infer N, number>}` ? N : never; // number (not round-trippable)
15+
type T7 = "10e2" extends `${Is<infer N, number>}` ? N : never; // number (not round-trippable)
16+
type T8 = "abcd" extends `${Is<infer N, number>}` ? N : never; // never
17+
18+
type T10 = "100" extends `${Is<infer N, bigint>}` ? N : never; // 100n
19+
type T11 = "-100" extends `${Is<infer N, bigint>}` ? N : never; // -100n
20+
type T12 = "0x10" extends `${Is<infer N, bigint>}` ? N : never; // bigint (not round-trippable)
21+
type T13 = "0o10" extends `${Is<infer N, bigint>}` ? N : never; // bigint (not round-trippable)
22+
type T14 = "0b10" extends `${Is<infer N, bigint>}` ? N : never; // bigint (not round-trippable)
23+
type T15 = "1.1" extends `${Is<infer N, bigint>}` ? N : never; // never
24+
type T16 = "10e2" extends `${Is<infer N, bigint>}` ? N : never; // never
25+
type T17 = "abcd" extends `${Is<infer N, bigint>}` ? N : never; // never
26+
27+
type T20 = "true" extends `${Is<infer T, boolean>}` ? T : never; // true
28+
type T21 = "false" extends `${Is<infer T, boolean>}` ? T : never; // false
29+
type T22 = "abcd" extends `${Is<infer T, boolean>}` ? T : never; // never
30+
31+
type T30 = "null" extends `${Is<infer T, null>}` ? T : never; // null
32+
type T31 = "abcd" extends `${Is<infer T, null>}` ? T : never; // never
33+
34+
type T40 = "undefined" extends `${Is<infer T, undefined>}` ? T : never; // undefined
35+
type T41 = "abcd" extends `${Is<infer T, undefined>}` ? T : never; // never
36+
37+
type T50 = "100" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "100" | 100 | 100n
38+
type T51 = "1.1" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "100" | 1.1
39+
type T52 = "true" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "true" | true
40+
type T53 = "false" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "false" | false
41+
type T54 = "null" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "null" | null
42+
type T55 = "undefined" extends `${Is<infer T, string | number | bigint | boolean | null | undefined>}` ? T : never; // "undefined" | undefined
43+
44+
type NumberFor<S extends string> = S extends `${Is<infer N, number>}` ? N : never;
45+
type T60 = NumberFor<"100">; // 100
46+
type T61 = NumberFor<any>; // never
47+
type T62 = NumberFor<never>; // never
48+
49+
// example use case:
50+
interface FieldDefinition {
51+
readonly name: string;
52+
readonly type: "i8" | "i16" | "i32" | "i64" | "u8" | "u16" | "u32" | "u64" | "f32" | "f64";
53+
}
54+
55+
type FieldType<T extends FieldDefinition["type"]> =
56+
T extends "i8" | "i16" | "i32" | "u8" | "u16" | "u32" | "f32" | "f64" ? number :
57+
T extends "f32" | "f64" ? bigint :
58+
never;
59+
60+
// Generates named members like `{ x: number, y: bigint }` from `[{ name: "x", type: "i32" }, { name: "y", type: "i64" }]`
61+
type TypedObjectNamedMembers<TDef extends readonly FieldDefinition[]> = {
62+
[P in TDef[number]["name"]]: FieldType<Extract<TDef[number], { readonly name: P }>["type"]>;
63+
};
64+
65+
// Generates ordinal members like `{ 0: number, 1: bigint }` from `[{ name: "x", type: "i32" }, { name: "y", type: "i64" }]`
66+
type TypedObjectOrdinalMembers<TDef extends readonly FieldDefinition[]> = {
67+
[I in Extract<keyof TDef, `${number}`>]: FieldType<Extract<TDef[I], FieldDefinition>["type"]>;
68+
};
69+
70+
// Default members
71+
interface TypedObjectMembers<TDef extends readonly FieldDefinition[]> {
72+
// get/set a field by name
73+
get<K extends TDef[number]["name"]>(key: K): FieldType<Extract<TDef[number], { readonly name: K }>["type"]>;
74+
set<K extends TDef[number]["name"]>(key: K, value: FieldType<Extract<TDef[number], { readonly name: K }>["type"]>): void;
75+
76+
// get/set a field by index
77+
getIndex<I extends IndicesOf<TDef>>(index: I): FieldType<Extract<TDef[I], FieldDefinition>["type"]>;
78+
setIndex<I extends IndicesOf<TDef>>(index: I, value: FieldType<Extract<TDef[I], FieldDefinition>["type"]>): void;
79+
}
80+
81+
// Use constrained `infer` in template literal to get ordinal indices as numbers:
82+
type IndicesOf<T> = NumberFor<Extract<keyof T, string>>; // ordinal indices as number literals
83+
84+
type TypedObject<TDef extends readonly FieldDefinition[]> =
85+
& TypedObjectMembers<TDef>
86+
& TypedObjectNamedMembers<TDef>
87+
& TypedObjectOrdinalMembers<TDef>;
88+
89+
// NOTE: type would normally be created from something like `const Point = TypedObject([...])` from which we would infer the type
90+
type Point = TypedObject<[
91+
{ name: "x", type: "f64" },
92+
{ name: "y", type: "f64" },
93+
]>;
94+
95+
declare const p: Point;
96+
p.getIndex(0); // ok, 0 is a valid index
97+
p.getIndex(1); // ok, 1 is a valid index
98+
p.getIndex(2); // error, 2 is not a valid index
99+
~
100+
!!! error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
101+
102+
p.setIndex(0, 0); // ok, 0 is a valid index
103+
p.setIndex(1, 0); // ok, 1 is a valid index
104+
p.setIndex(2, 3); // error, 2 is not a valid index
105+
~
106+
!!! error TS2345: Argument of type '2' is not assignable to parameter of type '0 | 1'.
107+

0 commit comments

Comments
 (0)