-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Fix conditional type type parameter leak #31455
Changes from 2 commits
3c1eef3
83b226a
9133ec5
60167a6
4d8ad93
6c86917
76ae07b
b636e3c
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 |
---|---|---|
|
@@ -10406,14 +10406,36 @@ namespace ts { | |
const checkTypeInstantiable = maybeTypeOfKind(checkType, TypeFlags.Instantiable | TypeFlags.GenericMappedType); | ||
let combinedMapper: TypeMapper | undefined; | ||
if (root.inferTypeParameters) { | ||
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None); | ||
// When we're looking at making an inference for an infer type, when we get its constraint, it'll automagically be | ||
// instantiated with the context, so it doesn't need the mapper for the inference contex - however the constraint | ||
// may refer to another _root_, _uncloned_ `infer` type parameter [1], or to something mapped by `mapper` [2]. | ||
// [1] Eg, if we have `Foo<T, U extends T>` and `Foo<number, infer B>` - `B` is constrained to `T`, which, in turn, has been instantiated | ||
// as `number` | ||
// Conversely, if we have `Foo<infer A, infer B>`, `B` is still constrained to `T` and `T` is instantiated as `A` | ||
// [2] Eg, if we have `Foo<T, U extends T>` and `Foo<Q, infer B>` where `Q` is mapped by `mapper` into `number` - `B` is constrained to `T` | ||
// which is in turn instantiated as `Q`, which is in turn instantiated as `number`. | ||
weswigham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// So we need to: | ||
weswigham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// * Clone the type parameters so their constraints can be instantiated in the context of `mapper` (otherwise theyd only get inference context information) | ||
// * Set the clones to both map the conditional's enclosing `mapper` and the original params | ||
// * instantiate the extends type with the clones | ||
// * incorporate all of the component mappers into the combined mapper for the true and false members | ||
weswigham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// This means we have three mappers that need applying: | ||
// * The original `mapper` used to create this conditional | ||
weswigham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// * The mapper that maps the old root type parameter to the clone (`freshMapper`) | ||
// * The mapper that maps the clone to its inference result (`context.mapper`) | ||
const freshParams = map(root.inferTypeParameters, cloneTypeParameter); | ||
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. oh no it happened 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. I'm concerned about all the new type identities created by always cloning the type parameters here. Fresh type parameters mean more work and more bloat in relationship caches. I'm wondering if we can somehow realize that we don't need to clone as that clearly covers the vast majority of cases. 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. We could avoid clone type parameters which aren't constrained to an instantiable type, for what that'd give us. |
||
const freshMapper = createTypeMapper(root.inferTypeParameters, freshParams); | ||
const context = createInferenceContext(freshParams, /*signature*/ undefined, InferenceFlags.None); | ||
for (const p of freshParams) { | ||
p.mapper = combineTypeMappers(mapper, freshMapper); | ||
} | ||
weswigham marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!checkTypeInstantiable) { | ||
// We don't want inferences from constraints as they may cause us to eagerly resolve the | ||
// conditional type instead of deferring resolution. Also, we always want strict function | ||
// types rules (i.e. proper contravariance) for inferences. | ||
inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict); | ||
inferTypes(context.inferences, checkType, instantiateType(extendsType, freshMapper), InferencePriority.NoConstraints | InferencePriority.AlwaysStrict); | ||
} | ||
combinedMapper = combineTypeMappers(mapper, context.mapper); | ||
combinedMapper = combineTypeMappers(combineTypeMappers(freshMapper, context.mapper), mapper); | ||
} | ||
// Instantiate the extends type including inferences for 'infer T' type parameters | ||
const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
tests/cases/compiler/conditionalDoesntLeakUninstantiatedTypeParameter.ts(7,7): error TS2322: Type '"3"' is not assignable to type 'number'. | ||
|
||
|
||
==== tests/cases/compiler/conditionalDoesntLeakUninstantiatedTypeParameter.ts (1 errors) ==== | ||
interface Synthetic<A, B extends A> {} | ||
type SyntheticDestination<T, U> = U extends Synthetic<T, infer V> ? V : never; | ||
type TestSynthetic = // Resolved to T, should be `number` or an inference failure (`unknown`) | ||
SyntheticDestination<number, Synthetic<number, number>>; | ||
|
||
const y: TestSynthetic = 3; // Type '3' is not assignable to type 'T'. (shouldn't error) | ||
const z: TestSynthetic = '3'; // Type '"3""' is not assignable to type 'T'. (should not mention T) | ||
~ | ||
!!! error TS2322: Type '"3"' is not assignable to type 'number'. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
//// [conditionalDoesntLeakUninstantiatedTypeParameter.ts] | ||
interface Synthetic<A, B extends A> {} | ||
type SyntheticDestination<T, U> = U extends Synthetic<T, infer V> ? V : never; | ||
type TestSynthetic = // Resolved to T, should be `number` or an inference failure (`unknown`) | ||
SyntheticDestination<number, Synthetic<number, number>>; | ||
|
||
const y: TestSynthetic = 3; // Type '3' is not assignable to type 'T'. (shouldn't error) | ||
const z: TestSynthetic = '3'; // Type '"3""' is not assignable to type 'T'. (should not mention T) | ||
|
||
|
||
//// [conditionalDoesntLeakUninstantiatedTypeParameter.js] | ||
var y = 3; // Type '3' is not assignable to type 'T'. (shouldn't error) | ||
var z = '3'; // Type '"3""' is not assignable to type 'T'. (should not mention T) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
=== tests/cases/compiler/conditionalDoesntLeakUninstantiatedTypeParameter.ts === | ||
interface Synthetic<A, B extends A> {} | ||
>Synthetic : Symbol(Synthetic, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 0, 0)) | ||
>A : Symbol(A, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 0, 20)) | ||
>B : Symbol(B, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 0, 22)) | ||
>A : Symbol(A, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 0, 20)) | ||
|
||
type SyntheticDestination<T, U> = U extends Synthetic<T, infer V> ? V : never; | ||
>SyntheticDestination : Symbol(SyntheticDestination, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 0, 38)) | ||
>T : Symbol(T, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 26)) | ||
>U : Symbol(U, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 28)) | ||
>U : Symbol(U, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 28)) | ||
>Synthetic : Symbol(Synthetic, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 0, 0)) | ||
>T : Symbol(T, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 26)) | ||
>V : Symbol(V, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 62)) | ||
>V : Symbol(V, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 62)) | ||
|
||
type TestSynthetic = // Resolved to T, should be `number` or an inference failure (`unknown`) | ||
>TestSynthetic : Symbol(TestSynthetic, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 78)) | ||
|
||
SyntheticDestination<number, Synthetic<number, number>>; | ||
>SyntheticDestination : Symbol(SyntheticDestination, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 0, 38)) | ||
>Synthetic : Symbol(Synthetic, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 0, 0)) | ||
|
||
const y: TestSynthetic = 3; // Type '3' is not assignable to type 'T'. (shouldn't error) | ||
>y : Symbol(y, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 5, 5)) | ||
>TestSynthetic : Symbol(TestSynthetic, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 78)) | ||
|
||
const z: TestSynthetic = '3'; // Type '"3""' is not assignable to type 'T'. (should not mention T) | ||
>z : Symbol(z, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 6, 5)) | ||
>TestSynthetic : Symbol(TestSynthetic, Decl(conditionalDoesntLeakUninstantiatedTypeParameter.ts, 1, 78)) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
=== tests/cases/compiler/conditionalDoesntLeakUninstantiatedTypeParameter.ts === | ||
interface Synthetic<A, B extends A> {} | ||
type SyntheticDestination<T, U> = U extends Synthetic<T, infer V> ? V : never; | ||
>SyntheticDestination : SyntheticDestination<T, U> | ||
|
||
type TestSynthetic = // Resolved to T, should be `number` or an inference failure (`unknown`) | ||
>TestSynthetic : number | ||
|
||
SyntheticDestination<number, Synthetic<number, number>>; | ||
|
||
const y: TestSynthetic = 3; // Type '3' is not assignable to type 'T'. (shouldn't error) | ||
>y : number | ||
>3 : 3 | ||
|
||
const z: TestSynthetic = '3'; // Type '"3""' is not assignable to type 'T'. (should not mention T) | ||
>z : number | ||
>'3' : "3" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
interface Synthetic<A, B extends A> {} | ||
type SyntheticDestination<T, U> = U extends Synthetic<T, infer V> ? V : never; | ||
type TestSynthetic = // Resolved to T, should be `number` or an inference failure (`unknown`) | ||
SyntheticDestination<number, Synthetic<number, number>>; | ||
|
||
const y: TestSynthetic = 3; // Type '3' is not assignable to type 'T'. (shouldn't error) | ||
const z: TestSynthetic = '3'; // Type '"3""' is not assignable to type 'T'. (should not mention T) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a more concise way to write this whole block? This right here is "here be dragons" commenting level Double Dragon. I expect demons to fly out of my nose after reading it.