Skip to content

Infer type parameters from indexes on those parameters #20126

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

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11197,7 +11197,8 @@ namespace ts {
inferredType: undefined,
priority: undefined,
topLevel: true,
isFixed: false
isFixed: false,
indexes: undefined,
};
}

Expand All @@ -11208,7 +11209,8 @@ namespace ts {
inferredType: inference.inferredType,
priority: inference.priority,
topLevel: inference.topLevel,
isFixed: inference.isFixed
isFixed: inference.isFixed,
indexes: inference.indexes && inference.indexes.slice(),
};
}

Expand Down Expand Up @@ -11430,6 +11432,23 @@ namespace ts {
inferFromTypes((<IndexedAccessType>source).objectType, (<IndexedAccessType>target).objectType);
inferFromTypes((<IndexedAccessType>source).indexType, (<IndexedAccessType>target).indexType);
}
else if (target.flags & TypeFlags.IndexedAccess) {
const targetConstraint = (<IndexedAccessType>target).objectType;
const inference = getInferenceInfoForType(targetConstraint);
if (inference) {
if (!inference.isFixed) {
// Instantiates instance of `type PartialInference<T, Keys extends string> = ({[K in Keys]: {[K1 in K]: T}})[Keys];`
// Where `T` is `source` and `Keys` is `target.indexType`
const inferenceTypeSymbol = getGlobalSymbol("PartialInference" as __String, SymbolFlags.Type, Diagnostics.Cannot_find_global_type_0);
const inferenceType = getDeclaredTypeOfSymbol(inferenceTypeSymbol);
if (inferenceType !== unknownType) {
const mapper = createTypeMapper(getSymbolLinks(inferenceTypeSymbol).typeParameters, [source, (target as IndexedAccessType).indexType]);
(inference.indexes || (inference.indexes = [])).push(instantiateType(inferenceType, mapper));
}
}
return;
}
}
else if (target.flags & TypeFlags.UnionOrIntersection) {
const targetTypes = (<UnionOrIntersectionType>target).types;
let typeVariableCount = 0;
Expand Down Expand Up @@ -11665,6 +11684,39 @@ namespace ts {
const inference = context.inferences[index];
let inferredType = inference.inferredType;
if (!inferredType) {
if (inference.indexes) {
// Build a candidate from all indexes
let aggregateInference = getIntersectionType(inference.indexes);
const constraint = getConstraintOfTypeParameter(context.signature.typeParameters[index]);
if (constraint) {
const instantiatedConstraint = instantiateType(constraint, context);
if (instantiatedConstraint.flags & TypeFlags.Union && !context.compareTypes(aggregateInference, getTypeWithThisArgument(instantiatedConstraint, aggregateInference))) {
const discriminantProps = findDiscriminantProperties(getPropertiesOfType(aggregateInference), instantiatedConstraint);
if (discriminantProps) {
let match: Type;
findDiscriminant: for (const p of discriminantProps) {
const candidatePropType = getTypeOfPropertyOfType(aggregateInference, p.escapedName);
for (const type of (instantiatedConstraint as UnionType).types) {
const propType = getTypeOfPropertyOfType(type, p.escapedName);
if (propType && checkTypeAssignableTo(candidatePropType, propType, /*errorNode*/ undefined)) {
if (match && match !== type) {
match = undefined;
break findDiscriminant;
}
else {
match = type;
}
}
}
}
if (match) {
aggregateInference = getSpreadType(match, aggregateInference, /*symbol*/ undefined, /*propegatedFlags*/ 0);
}
}
}
}
(inference.candidates || (inference.candidates = [])).push(aggregateInference);
}
if (inference.candidates) {
// Extract all object literal types and replace them with a single widened and normalized type.
const candidates = widenObjectLiteralCandidates(inference.candidates);
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3594,6 +3594,9 @@ namespace ts {
constraintType?: Type;
templateType?: Type;
modifiersType?: Type;
hasQuestionToken?: boolean;
hasReadonlyToken?: boolean;
hasPossiblyHomomorphicConstraint?: boolean;
}

export interface EvolvingArrayType extends ObjectType {
Expand Down Expand Up @@ -3744,6 +3747,7 @@ namespace ts {
export interface InferenceInfo {
typeParameter: TypeParameter;
candidates: Type[];
indexes: Type[]; // Partial candidates created by indexed accesses
inferredType: Type;
priority: InferencePriority;
topLevel: boolean;
Expand Down
9 changes: 9 additions & 0 deletions src/lib/es5.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,15 @@ type Record<K extends string, T> = {
*/
interface ThisType<T> { }

/**
* Type instantiated to perform partial inferences from indexed accesses
*/
type PartialInference<T, Keys extends string> = ({
[K in Keys]: {
[K1 in K]: T
}
})[Keys];

/**
* Represents a raw buffer of binary data, which is used to store data for the
* different typed arrays. ArrayBuffers cannot be read from or written to directly,
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/tsserverlibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,7 @@ declare namespace ts {
interface InferenceInfo {
typeParameter: TypeParameter;
candidates: Type[];
indexes: Type[];
inferredType: Type;
priority: InferencePriority;
topLevel: boolean;
Expand Down
1 change: 1 addition & 0 deletions tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2154,6 +2154,7 @@ declare namespace ts {
interface InferenceInfo {
typeParameter: TypeParameter;
candidates: Type[];
indexes: Type[];
inferredType: Type;
priority: InferencePriority;
topLevel: boolean;
Expand Down
148 changes: 148 additions & 0 deletions tests/baselines/reference/indexAccessCombinedInference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//// [indexAccessCombinedInference.ts]
// Simple case
interface Args {
TA: object,
TY: object
}

declare function foo<T extends Args>(
a: T["TA"],
b: T["TY"]): T["TA"] & T["TY"];

const x = foo({
x: {
j: 12,
i: 11
}
}, { y: 42 });

// Union result type
interface A {
foo: number;
}
interface B {
bar: string;
}
declare const something: A | B;

const y = foo(something, { bat: 42 });

// Union key type
interface Args2 {
TA?: object, // Optional since only one of TA or TB needs to be infered in the below argument list
TB?: object,
TY: object
}
declare function foo2<T extends Args2>(
a: T["TA"] | T["TB"],
b: T["TY"]): {a: T["TA"], b: T["TB"]} & T["TY"];
declare function foo3<T extends Args2>( // Morally equivalent to foo2
a: T["TA" | "TB"],
b: T["TY"]): {a: T["TA"], b: T["TB"]} & T["TY"];
let z = foo2({
x: {
j: 12,
i: 11
}
}, { y: 42 });
let zz = foo3({
x: {
j: 12,
i: 11
}
}, { y: 42 });
z = zz;
zz = z;

// Higher-order
interface Args3 {
Key: "A" | "B",
A: object,
B: object,
Merge: object,
}
declare const either: "A" | "B";
declare function pickOne<T extends Args3>(key: T["Key"], left: T["A"], right: T["B"], into: T["Merge"]): T[T["Key"]] & T["Merge"];

const opt1 = pickOne("A", {x: 12}, {y: ""}, {z: /./});
const opt2 = pickOne("B", {x: 12}, {y: ""}, {z: /./});
const opt3 = pickOne(either, {x: 12}, {y: ""}, {z: /./});

const pickDelayed = <TKey extends Args3["Key"]>(x: TKey) => pickOne(x, {j: x}, {i: x}, {chosen: x});
const opt4 = pickDelayed("A");
const opt5 = pickDelayed("B");
const opt6 = pickDelayed(either);

// Reopenable
interface Args3 {
/**
* One must make patched parameters optional, otherwise signatures expecting the unpatched
* interface (ie, pickOne above) will not be able to produce a type satisfying the interface
* (as there are no inference sites for the new members) and will fall back to the constraints on each member
*/
Extra?: object,
}
declare function pickOne<T extends Args3>(key: T["Key"], left: T["A"], right: T["B"], into: T["Merge"], extra: T["Extra"]): T[T["Key"]] & {into: T["Merge"], extra: T["Extra"]};
const opt7 = pickOne("A", {x: 12}, {y: ""}, {z: /./}, {z: /./});
const opt8 = pickOne("B", {x: 12}, {y: ""}, {z: /./}, {z: /./});
const opt9 = pickOne(either, {x: 12}, {y: ""}, {z: /./}, {z: /./});

// Interactions with `this` types
interface TPicker {
Key: keyof this,
X: number,
Y: string
}
declare function chooseLiteral<T extends TPicker>(choice: T["Key"], x: T["X"], y:T["Y"]): T[T["Key"]];
const cx = chooseLiteral("X", 1, "no");
const cy = chooseLiteral("Y", 0, "yes");
const ceither = chooseLiteral("X" as "X" | "Y", 1, "yes");
const cneither = chooseLiteral("Key", 0, "no");

// Multiple inference sites
interface Args4 {
0: object,
1: Record<keyof this["0"], Function>,
}
declare function dualInputs<T extends Args4>(x: T["0"], y: T["0"], toDelay: T["1"]): T["0"] & {transformers: T["1"]};

const result = dualInputs({x: 0}, {x: 1}, {x: () => ""});


//// [indexAccessCombinedInference.js]
var x = foo({
x: {
j: 12,
i: 11
}
}, { y: 42 });
var y = foo(something, { bat: 42 });
var z = foo2({
x: {
j: 12,
i: 11
}
}, { y: 42 });
var zz = foo3({
x: {
j: 12,
i: 11
}
}, { y: 42 });
z = zz;
zz = z;
var opt1 = pickOne("A", { x: 12 }, { y: "" }, { z: /./ });
var opt2 = pickOne("B", { x: 12 }, { y: "" }, { z: /./ });
var opt3 = pickOne(either, { x: 12 }, { y: "" }, { z: /./ });
var pickDelayed = function (x) { return pickOne(x, { j: x }, { i: x }, { chosen: x }); };
var opt4 = pickDelayed("A");
var opt5 = pickDelayed("B");
var opt6 = pickDelayed(either);
var opt7 = pickOne("A", { x: 12 }, { y: "" }, { z: /./ }, { z: /./ });
var opt8 = pickOne("B", { x: 12 }, { y: "" }, { z: /./ }, { z: /./ });
var opt9 = pickOne(either, { x: 12 }, { y: "" }, { z: /./ }, { z: /./ });
var cx = chooseLiteral("X", 1, "no");
var cy = chooseLiteral("Y", 0, "yes");
var ceither = chooseLiteral("X", 1, "yes");
var cneither = chooseLiteral("Key", 0, "no");
var result = dualInputs({ x: 0 }, { x: 1 }, { x: function () { return ""; } });
Loading