Skip to content

Infer type parameters from indexes on those parameters #53017

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

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3451575
Infer type parameters from indexes on those parameters
weswigham Nov 18, 2017
f5b208c
Greatly simplify partial inference type instantiation through use of …
weswigham Nov 22, 2017
ba064ac
Add many more tests showing current behaviors
weswigham Nov 22, 2017
93afc10
Discriminate partial inferences if not complete enough to satisfy con…
weswigham Dec 1, 2017
49e1961
Move case to prefered location
weswigham Dec 21, 2017
96772e5
Small refactor to reduce how many comparisons are performed
weswigham Dec 21, 2017
2e0c635
Infer reverse mapped types based on inferences made for its concrete …
Andarist Feb 28, 2023
7b8cf10
Merge remote-tracking branch 'origin/main' into infer-concrete-proper…
Andarist Apr 17, 2023
e7e4e70
Merge remote-tracking branch 'weswigham/index-combined-inferences' in…
Andarist Apr 17, 2023
d0e8b8b
update baselines, note: some are broken
Andarist Apr 17, 2023
e5d0ea8
use new `inference.indexes` in reverse mapped type inference
Andarist Apr 17, 2023
b0b2fcb
update reverse mapped baseline
Andarist Apr 18, 2023
7fe118a
Call `getActualTypeVariable` appropriately to fix inference for a tup…
Andarist Apr 18, 2023
50e6a0c
Avoid inferring an index under `InferencePriority.NakedTypeVariable`
Andarist Apr 18, 2023
4febb9c
always discard aggregate inference that is not assignable to the cons…
Andarist Apr 19, 2023
c2d4b0f
bring back the union discriminating logic
Andarist Apr 19, 2023
11b02d0
fixed incorrect indexed access in tests
Andarist Apr 19, 2023
97adac5
only use the aggregate inference when there are no other candidates
Andarist Apr 19, 2023
8ffcb94
do not collect index-based inferences from non-inferrable types
Andarist Apr 20, 2023
192d8b7
Merge remote-tracking branch 'origin/main' into infer-concrete-proper…
Andarist May 22, 2023
5fa585c
Merge remote-tracking branch 'origin/main' into infer-concrete-proper…
Andarist Jun 13, 2023
375c127
Merge remote-tracking branch 'origin/main' into infer-concrete-proper…
Andarist Aug 22, 2023
a62749c
Merge remote-tracking branch 'origin/main' into infer-concrete-proper…
Andarist Sep 15, 2024
2f86371
Merge remote-tracking branch 'origin/main' into infer-concrete-proper…
Andarist Sep 16, 2024
9825bcd
update baselines
Andarist Sep 16, 2024
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
58 changes: 55 additions & 3 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23945,7 +23945,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
priority: undefined,
topLevel: true,
isFixed: false,
impliedArity: undefined
impliedArity: undefined,
indexes: undefined
};
}

Expand All @@ -23958,7 +23959,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
priority: inference.priority,
topLevel: inference.topLevel,
isFixed: inference.isFixed,
impliedArity: inference.impliedArity
impliedArity: inference.impliedArity,
indexes: inference.indexes && inference.indexes.slice()
};
}

Expand Down Expand Up @@ -24106,7 +24108,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const templateType = getTemplateTypeFromMappedType(target);
const inference = createInferenceInfo(typeParameter);
inferTypes([inference], sourceType, templateType);
return getTypeFromInference(inference) || unknownType;
return getTypeFromInference(inference) || (inference.indexes ? getIntersectionType(inference.indexes) : unknownType);
}

function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol> {
Expand Down Expand Up @@ -24509,6 +24511,23 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
inferFromTypes((source as IndexedAccessType).objectType, (target as IndexedAccessType).objectType);
inferFromTypes((source as IndexedAccessType).indexType, (target as IndexedAccessType).indexType);
}
else if (!(priority & InferencePriority.NakedTypeVariable) && target.flags & TypeFlags.IndexedAccess) {
const targetConstraint = (target as IndexedAccessType).objectType;
const inference = getInferenceInfoForType(getActualTypeVariable(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 = inferenceTypeSymbol && getDeclaredTypeOfSymbol(inferenceTypeSymbol);
if (inferenceType && inferenceType !== unknownType) {
const mapper = createTypeMapper(getSymbolLinks(inferenceTypeSymbol).typeParameters!, [source, (target as IndexedAccessType).indexType]);
(inference.indexes || (inference.indexes = [])).push(instantiateType(inferenceType, mapper));
}
}
return;
}
}
else if (source.flags & TypeFlags.StringMapping && target.flags & TypeFlags.StringMapping) {
if ((source as StringMappingType).symbol === (target as StringMappingType).symbol) {
inferFromTypes((source as StringMappingType).type, (target as StringMappingType).type);
Expand Down Expand Up @@ -25141,6 +25160,39 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
let inferredType: Type | undefined;
const signature = context.signature;
if (signature) {
if (inference.indexes) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the time of this comment this aggregated inference is pushed as an extra candidate (that's the behavior preserved from #20126 ).

That creates problems with cases like this:

interface Expression extends Node {
    _expressionBrand: any;
}

declare function setParent<T extends Node>(child: T, parent: T["parent"] | undefined): T;

interface Node {
    readonly kind: number;
    readonly parent: Node;
}

declare const expr: Expression
declare const node: Node

const res = setParent(expr, node)
//    ^? actual: Node, expected: Expression

export {}

It incorrectly widens the inferred type because it's able to eliminate the inferred subtype (Expression) through subtype reduction~.

I was afraid of cases like this in my initial implementation and in there I decided to just use the gathered indexes~ when there were no candidates for the "naked" type variable. Should I do that here and avoid making the intersected indexes the "same priority" candidate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, this is what happens right now:

  1. we end up with 2 candidates (because the aggregateInference is appended as an extra candidate): Expression and { parent: Node; }
  2. we compute a common super type (getCommonSupertype(baseCandidates)) which is { parent: Node; }
  3. that is not assignable to the constraint, it fails the compareTypes check and thus we assign the instantiated constraint as the inferred type (inference.inferredType = inferredType = instantiatedConstraint)
  4. that in turn is just OK further down the road since the constraint (Node) satisfies everything here

I tried to just always require the aggregateInference to be comparable to the constraint but that broke the "discriminating logic" added for this aggregateInference stuff. So I reverted that change and only added this requirement for non-union constraints.

At this point, the logic related to pushing aggregateInference into the inference.candidates is a little bit hairy 🤷‍♂️ I guess the main question here is if every bit of the included enhancement is good at the end of the day, how it fits the rest of the compiler etc. That's something on what you'd have to chime in with @ahejlsberg .

I still have some of those change to investigate but at least the self-check is working now so perhaps we could create a build using this PR for toying purposes.

// Build a candidate from all indexes
let aggregateInference = getIntersectionType(inference.indexes);
const constraint = getConstraintOfTypeParameter(signature.typeParameters![index]);
if (constraint) {
const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper);
if (instantiatedConstraint.flags & TypeFlags.Union && !context.compareTypes(aggregateInference, getTypeWithThisArgument(instantiatedConstraint, aggregateInference))) {
const discriminantProps = findDiscriminantProperties(getPropertiesOfType(aggregateInference), instantiatedConstraint);
if (discriminantProps) {
let match: Type | undefined;
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 && candidatePropType && 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, /*readonly*/ false);
}
}
}
}
(inference.candidates || (inference.candidates = [])).push(aggregateInference);
}
const inferredCovariantType = inference.candidates ? getCovariantInference(inference, signature) : undefined;
if (inference.contraCandidates) {
// If we have both co- and contra-variant inferences, we use the co-variant inference if it is not 'never',
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6826,6 +6826,7 @@ export interface InferenceInfo {
typeParameter: TypeParameter; // Type parameter for which inferences are being made
candidates: Type[] | undefined; // Candidates in covariant positions (or undefined)
contraCandidates: Type[] | undefined; // Candidates in contravariant positions (or undefined)
indexes: Type[] | undefined; // Partial candidates created by indexed accesses
inferredType?: Type; // Cache for resolved inferred type
priority?: InferencePriority; // Priority of current inference set
topLevel: boolean; // True if all inferences are to top level occurrences
Expand Down
1 change: 1 addition & 0 deletions src/harness/fourslashInterfaceImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,7 @@ export namespace Completion {
typeEntry("Capitalize"),
typeEntry("Uncapitalize"),
interfaceEntry("ThisType"),
typeEntry("PartialInference"),
varEntry("ArrayBuffer"),
interfaceEntry("ArrayBufferTypes"),
typeEntry("ArrayBufferLike"),
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 @@ -1647,6 +1647,15 @@ type Uncapitalize<S extends string> = intrinsic;
*/
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
10 changes: 5 additions & 5 deletions tests/baselines/reference/correlatedUnions.types
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ function ff1() {
>Keys : keyof { sum: [a: number, b: number]; concat: [a: string, b: string, c: string]; }

const funs: { [P in Keys]: (...args: ArgMap[P]) => void } = {
>funs : { concat: (a: string, b: string, c: string) => void; sum: (a: number, b: number) => void; }
>funs : { sum: (a: number, b: number) => void; concat: (a: string, b: string, c: string) => void; }
>args : { sum: [a: number, b: number]; concat: [a: string, b: string, c: string]; }[P]
>{ sum: (a, b) => a + b, concat: (a, b, c) => a + b + c } : { sum: (a: number, b: number) => number; concat: (a: string, b: string, c: string) => string; }

Expand Down Expand Up @@ -520,14 +520,14 @@ function ff1() {
>args : { sum: [a: number, b: number]; concat: [a: string, b: string, c: string]; }[K]

const fn = funs[funKey];
>fn : { concat: (a: string, b: string, c: string) => void; sum: (a: number, b: number) => void; }[K]
>funs[funKey] : { concat: (a: string, b: string, c: string) => void; sum: (a: number, b: number) => void; }[K]
>funs : { concat: (a: string, b: string, c: string) => void; sum: (a: number, b: number) => void; }
>fn : { sum: (a: number, b: number) => void; concat: (a: string, b: string, c: string) => void; }[K]
>funs[funKey] : { sum: (a: number, b: number) => void; concat: (a: string, b: string, c: string) => void; }[K]
>funs : { sum: (a: number, b: number) => void; concat: (a: string, b: string, c: string) => void; }
>funKey : K

fn(...args);
>fn(...args) : void
>fn : { concat: (a: string, b: string, c: string) => void; sum: (a: number, b: number) => void; }[K]
>fn : { sum: (a: number, b: number) => void; concat: (a: string, b: string, c: string) => void; }[K]
>...args : string | number
>args : { sum: [a: number, b: number]; concat: [a: string, b: string, c: string]; }[K]
}
Expand Down
130 changes: 130 additions & 0 deletions tests/baselines/reference/indexAccessCombinedInference.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
tests/cases/compiler/indexAccessCombinedInference.ts(104,21): error TS2536: Type '"0"' cannot be used to index type 'this'.
tests/cases/compiler/indexAccessCombinedInference.ts(106,49): error TS2536: Type '"0"' cannot be used to index type 'T'.
tests/cases/compiler/indexAccessCombinedInference.ts(106,60): error TS2536: Type '"0"' cannot be used to index type 'T'.
tests/cases/compiler/indexAccessCombinedInference.ts(106,77): error TS2536: Type '"1"' cannot be used to index type 'T'.
tests/cases/compiler/indexAccessCombinedInference.ts(106,86): error TS2536: Type '"0"' cannot be used to index type 'T'.
tests/cases/compiler/indexAccessCombinedInference.ts(106,110): error TS2536: Type '"1"' cannot be used to index type 'T'.


==== tests/cases/compiler/indexAccessCombinedInference.ts (6 errors) ====
// 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>,
~~~~~~~~~
!!! error TS2536: Type '"0"' cannot be used to index type 'this'.
}
declare function dualInputs<T extends Args4>(x: T["0"], y: T["0"], toDelay: T["1"]): T["0"] & {transformers: T["1"]};
~~~~~~
!!! error TS2536: Type '"0"' cannot be used to index type 'T'.
~~~~~~
!!! error TS2536: Type '"0"' cannot be used to index type 'T'.
~~~~~~
!!! error TS2536: Type '"1"' cannot be used to index type 'T'.
~~~~~~
!!! error TS2536: Type '"0"' cannot be used to index type 'T'.
~~~~~~
!!! error TS2536: Type '"1"' cannot be used to index type 'T'.

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

Loading