-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
base: main
Are you sure you want to change the base?
Changes from 14 commits
3451575
f5b208c
ba064ac
93afc10
49e1961
96772e5
2e0c635
7b8cf10
e7e4e70
d0e8b8b
e5d0ea8
b0b2fcb
7fe118a
50e6a0c
4febb9c
c2d4b0f
11b02d0
97adac5
8ffcb94
192d8b7
5fa585c
375c127
a62749c
2f86371
9825bcd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23945,7 +23945,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
priority: undefined, | ||
topLevel: true, | ||
isFixed: false, | ||
impliedArity: undefined | ||
impliedArity: undefined, | ||
indexes: undefined | ||
}; | ||
} | ||
|
||
|
@@ -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() | ||
}; | ||
} | ||
|
||
|
@@ -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> { | ||
|
@@ -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); | ||
|
@@ -25141,6 +25160,39 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
let inferredType: Type | undefined; | ||
const signature = context.signature; | ||
if (signature) { | ||
if (inference.indexes) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 ( I was afraid of cases like this in my initial implementation and in there I decided to just use the gathered There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To clarify, this is what happens right now:
I tried to just always require the At this point, the logic related to pushing 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', | ||
|
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: () => ""}); | ||
|
Uh oh!
There was an error while loading. Please reload this page.