From 05ac44f26cfcc1e4415c25d446e858ed65bd6c31 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 16 Oct 2018 16:10:41 -0700 Subject: [PATCH 1/4] Add assignability rules for when the target is a conditional type --- src/compiler/checker.ts | 25 +++++++++ .../reference/genericRestTypes.types | 2 +- .../thisConditionalInferenceInClassBody.js | 26 +++++++++ ...hisConditionalInferenceInClassBody.symbols | 53 +++++++++++++++++++ .../thisConditionalInferenceInClassBody.types | 40 ++++++++++++++ .../thisConditionalInferenceInClassBody.ts | 14 +++++ 6 files changed, 159 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/thisConditionalInferenceInClassBody.js create mode 100644 tests/baselines/reference/thisConditionalInferenceInClassBody.symbols create mode 100644 tests/baselines/reference/thisConditionalInferenceInClassBody.types create mode 100644 tests/cases/compiler/thisConditionalInferenceInClassBody.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 604805545a401..93b087ce8539d 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11955,6 +11955,31 @@ namespace ts { } } } + else if (target.flags & TypeFlags.Conditional) { + const root = (target as ConditionalType).root; + if (root.inferTypeParameters) { + // If the constraint indicates that the conditional type is always true (but it is stil deferred to allow for, eg, distribution or inference) + // We should perform the instantiation and only check against the true type + const mapper = (target as ConditionalType).mapper; + const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None); + const instantiatedExtends = instantiateType(root.extendsType, mapper); + const checkConstraint = getSimplifiedType(instantiateType(root.checkType, combineTypeMappers(mapper, getBaseConstraintOrType))); + inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict); + const combinedMapper = combineTypeMappers(mapper, context); + if (isRelatedTo(checkConstraint, instantiateType(root.extendsType, combinedMapper))) { + if (result = isRelatedTo(source, instantiateType(root.trueType, combinedMapper))) { + errorInfo = saveErrorInfo; + return result; + } + } + } + if (result = isRelatedTo(source, getTrueTypeFromConditionalType(target as ConditionalType))) { + if (result = isRelatedTo(source, getFalseTypeFromConditionalType(target as ConditionalType))) { + errorInfo = saveErrorInfo; + return result; + } + } + } if (source.flags & TypeFlags.TypeVariable) { if (source.flags & TypeFlags.IndexedAccess && target.flags & TypeFlags.IndexedAccess) { diff --git a/tests/baselines/reference/genericRestTypes.types b/tests/baselines/reference/genericRestTypes.types index b2fb37871bd23..49835b50b94e2 100644 --- a/tests/baselines/reference/genericRestTypes.types +++ b/tests/baselines/reference/genericRestTypes.types @@ -22,7 +22,7 @@ type Bind1 any> = (...args: TailBind1 : Bind1 >head : any >tail : any[] ->args : Tail> +>args : any[] type Generic = Bind1; // (bar: string) => boolean >Generic : Bind1 diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.js b/tests/baselines/reference/thisConditionalInferenceInClassBody.js new file mode 100644 index 0000000000000..a2f068410b2ff --- /dev/null +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.js @@ -0,0 +1,26 @@ +//// [thisConditionalInferenceInClassBody.ts] +type Wrapped = { ___secret: T }; +type Unwrap = T extends Wrapped ? U : T; + +declare function set(obj: T, key: K, value: Unwrap): Unwrap; + +class Foo { + prop: Wrapped; + + method() { + set(this, 'prop', 'hi'); // <-- type error + } +} + +set(new Foo(), 'prop', 'hi'); // <-- typechecks + +//// [thisConditionalInferenceInClassBody.js] +var Foo = /** @class */ (function () { + function Foo() { + } + Foo.prototype.method = function () { + set(this, 'prop', 'hi'); // <-- type error + }; + return Foo; +}()); +set(new Foo(), 'prop', 'hi'); // <-- typechecks diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols b/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols new file mode 100644 index 0000000000000..50cdd74b12212 --- /dev/null +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols @@ -0,0 +1,53 @@ +=== tests/cases/compiler/thisConditionalInferenceInClassBody.ts === +type Wrapped = { ___secret: T }; +>Wrapped : Symbol(Wrapped, Decl(thisConditionalInferenceInClassBody.ts, 0, 0)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 0, 13)) +>___secret : Symbol(___secret, Decl(thisConditionalInferenceInClassBody.ts, 0, 19)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 0, 13)) + +type Unwrap = T extends Wrapped ? U : T; +>Unwrap : Symbol(Unwrap, Decl(thisConditionalInferenceInClassBody.ts, 0, 35)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 1, 12)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 1, 12)) +>Wrapped : Symbol(Wrapped, Decl(thisConditionalInferenceInClassBody.ts, 0, 0)) +>U : Symbol(U, Decl(thisConditionalInferenceInClassBody.ts, 1, 40)) +>U : Symbol(U, Decl(thisConditionalInferenceInClassBody.ts, 1, 40)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 1, 12)) + +declare function set(obj: T, key: K, value: Unwrap): Unwrap; +>set : Symbol(set, Decl(thisConditionalInferenceInClassBody.ts, 1, 52)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 3, 21)) +>K : Symbol(K, Decl(thisConditionalInferenceInClassBody.ts, 3, 23)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 3, 21)) +>obj : Symbol(obj, Decl(thisConditionalInferenceInClassBody.ts, 3, 43)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 3, 21)) +>key : Symbol(key, Decl(thisConditionalInferenceInClassBody.ts, 3, 50)) +>K : Symbol(K, Decl(thisConditionalInferenceInClassBody.ts, 3, 23)) +>value : Symbol(value, Decl(thisConditionalInferenceInClassBody.ts, 3, 58)) +>Unwrap : Symbol(Unwrap, Decl(thisConditionalInferenceInClassBody.ts, 0, 35)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 3, 21)) +>K : Symbol(K, Decl(thisConditionalInferenceInClassBody.ts, 3, 23)) +>Unwrap : Symbol(Unwrap, Decl(thisConditionalInferenceInClassBody.ts, 0, 35)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 3, 21)) +>K : Symbol(K, Decl(thisConditionalInferenceInClassBody.ts, 3, 23)) + +class Foo { +>Foo : Symbol(Foo, Decl(thisConditionalInferenceInClassBody.ts, 3, 94)) + + prop: Wrapped; +>prop : Symbol(Foo.prop, Decl(thisConditionalInferenceInClassBody.ts, 5, 11)) +>Wrapped : Symbol(Wrapped, Decl(thisConditionalInferenceInClassBody.ts, 0, 0)) + + method() { +>method : Symbol(Foo.method, Decl(thisConditionalInferenceInClassBody.ts, 6, 26)) + + set(this, 'prop', 'hi'); // <-- type error +>set : Symbol(set, Decl(thisConditionalInferenceInClassBody.ts, 1, 52)) +>this : Symbol(Foo, Decl(thisConditionalInferenceInClassBody.ts, 3, 94)) + } +} + +set(new Foo(), 'prop', 'hi'); // <-- typechecks +>set : Symbol(set, Decl(thisConditionalInferenceInClassBody.ts, 1, 52)) +>Foo : Symbol(Foo, Decl(thisConditionalInferenceInClassBody.ts, 3, 94)) + diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.types b/tests/baselines/reference/thisConditionalInferenceInClassBody.types new file mode 100644 index 0000000000000..217d6d261551e --- /dev/null +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.types @@ -0,0 +1,40 @@ +=== tests/cases/compiler/thisConditionalInferenceInClassBody.ts === +type Wrapped = { ___secret: T }; +>Wrapped : Wrapped +>___secret : T + +type Unwrap = T extends Wrapped ? U : T; +>Unwrap : Unwrap + +declare function set(obj: T, key: K, value: Unwrap): Unwrap; +>set : (obj: T, key: K, value: Unwrap) => Unwrap +>obj : T +>key : K +>value : Unwrap + +class Foo { +>Foo : Foo + + prop: Wrapped; +>prop : Wrapped + + method() { +>method : () => void + + set(this, 'prop', 'hi'); // <-- type error +>set(this, 'prop', 'hi') : Unwrap +>set : (obj: T, key: K, value: Unwrap) => Unwrap +>this : this +>'prop' : "prop" +>'hi' : "hi" + } +} + +set(new Foo(), 'prop', 'hi'); // <-- typechecks +>set(new Foo(), 'prop', 'hi') : string +>set : (obj: T, key: K, value: Unwrap) => Unwrap +>new Foo() : Foo +>Foo : typeof Foo +>'prop' : "prop" +>'hi' : "hi" + diff --git a/tests/cases/compiler/thisConditionalInferenceInClassBody.ts b/tests/cases/compiler/thisConditionalInferenceInClassBody.ts new file mode 100644 index 0000000000000..7b6c1b2ccde37 --- /dev/null +++ b/tests/cases/compiler/thisConditionalInferenceInClassBody.ts @@ -0,0 +1,14 @@ +type Wrapped = { ___secret: T }; +type Unwrap = T extends Wrapped ? U : T; + +declare function set(obj: T, key: K, value: Unwrap): Unwrap; + +class Foo { + prop: Wrapped; + + method() { + set(this, 'prop', 'hi'); // <-- type error + } +} + +set(new Foo(), 'prop', 'hi'); // <-- typechecks \ No newline at end of file From 17e215c3caa7d3166928ccb5591d356c02562029 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 16 Oct 2018 17:06:21 -0700 Subject: [PATCH 2/4] Slightly improve test, add comment explaining situation --- src/compiler/checker.ts | 15 ++++++++++--- .../reference/genericRestTypes.types | 2 +- .../thisConditionalInferenceInClassBody.js | 12 +++++++++- ...hisConditionalInferenceInClassBody.symbols | 22 +++++++++++++++++++ .../thisConditionalInferenceInClassBody.types | 13 +++++++++++ .../thisConditionalInferenceInClassBody.ts | 8 ++++++- 6 files changed, 66 insertions(+), 6 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 93b087ce8539d..cd39634b15cb5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11963,11 +11963,20 @@ namespace ts { const mapper = (target as ConditionalType).mapper; const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None); const instantiatedExtends = instantiateType(root.extendsType, mapper); - const checkConstraint = getSimplifiedType(instantiateType(root.checkType, combineTypeMappers(mapper, getBaseConstraintOrType))); - inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict); + const checkConstraint = getSimplifiedType(instantiateType(root.checkType, mapper)); + // TODO: Use InferencePriority.NoConstraints for type parameters which are being used contravariantly/in a write-only context + // (Or, ideally, some future InferencePriority.SuperConstraints should we start tracking them!) + // As-is, this is unsound - it can be made stricter by replacing `instantiateType(root.trueType, combinedMapper)` with + // `getintersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)])` which + // would preserve the type-parametery-ness of the check; but such a limitation makes this branch almost useless, as only + // conditional types with effectively "independent" inference parameters will end up being assignable via this branch, eg + // `type InferBecauseWhyNot = T extends (p: infer P1) => any ? T | P1 : never;` + // contains a union in the `true` branch, and so while we can't confirm assignability to `P1`, we _could_ confirm assignability + // to `T`. + inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.AlwaysStrict); const combinedMapper = combineTypeMappers(mapper, context); if (isRelatedTo(checkConstraint, instantiateType(root.extendsType, combinedMapper))) { - if (result = isRelatedTo(source, instantiateType(root.trueType, combinedMapper))) { + if (result = isRelatedTo(source, instantiateType(root.trueType, combinedMapper), reportErrors)) { errorInfo = saveErrorInfo; return result; } diff --git a/tests/baselines/reference/genericRestTypes.types b/tests/baselines/reference/genericRestTypes.types index 49835b50b94e2..b2fb37871bd23 100644 --- a/tests/baselines/reference/genericRestTypes.types +++ b/tests/baselines/reference/genericRestTypes.types @@ -22,7 +22,7 @@ type Bind1 any> = (...args: TailBind1 : Bind1 >head : any >tail : any[] ->args : any[] +>args : Tail> type Generic = Bind1; // (bar: string) => boolean >Generic : Bind1 diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.js b/tests/baselines/reference/thisConditionalInferenceInClassBody.js index a2f068410b2ff..aa800d2f85b4d 100644 --- a/tests/baselines/reference/thisConditionalInferenceInClassBody.js +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.js @@ -12,7 +12,14 @@ class Foo { } } -set(new Foo(), 'prop', 'hi'); // <-- typechecks +set(new Foo(), 'prop', 'hi'); // <-- typechecks + +type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; + +function f any>(x: Q): InferBecauseWhyNot { + return x; +} + //// [thisConditionalInferenceInClassBody.js] var Foo = /** @class */ (function () { @@ -24,3 +31,6 @@ var Foo = /** @class */ (function () { return Foo; }()); set(new Foo(), 'prop', 'hi'); // <-- typechecks +function f(x) { + return x; +} diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols b/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols index 50cdd74b12212..bab2788cbae12 100644 --- a/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols @@ -51,3 +51,25 @@ set(new Foo(), 'prop', 'hi'); // <-- typechecks >set : Symbol(set, Decl(thisConditionalInferenceInClassBody.ts, 1, 52)) >Foo : Symbol(Foo, Decl(thisConditionalInferenceInClassBody.ts, 3, 94)) +type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; +>InferBecauseWhyNot : Symbol(InferBecauseWhyNot, Decl(thisConditionalInferenceInClassBody.ts, 13, 29)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 15, 24)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 15, 24)) +>p : Symbol(p, Decl(thisConditionalInferenceInClassBody.ts, 15, 40)) +>P1 : Symbol(P1, Decl(thisConditionalInferenceInClassBody.ts, 15, 48)) +>P1 : Symbol(P1, Decl(thisConditionalInferenceInClassBody.ts, 15, 48)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 15, 24)) + +function f any>(x: Q): InferBecauseWhyNot { +>f : Symbol(f, Decl(thisConditionalInferenceInClassBody.ts, 15, 77)) +>Q : Symbol(Q, Decl(thisConditionalInferenceInClassBody.ts, 17, 11)) +>arg : Symbol(arg, Decl(thisConditionalInferenceInClassBody.ts, 17, 22)) +>x : Symbol(x, Decl(thisConditionalInferenceInClassBody.ts, 17, 40)) +>Q : Symbol(Q, Decl(thisConditionalInferenceInClassBody.ts, 17, 11)) +>InferBecauseWhyNot : Symbol(InferBecauseWhyNot, Decl(thisConditionalInferenceInClassBody.ts, 13, 29)) +>Q : Symbol(Q, Decl(thisConditionalInferenceInClassBody.ts, 17, 11)) + + return x; +>x : Symbol(x, Decl(thisConditionalInferenceInClassBody.ts, 17, 40)) +} + diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.types b/tests/baselines/reference/thisConditionalInferenceInClassBody.types index 217d6d261551e..469f8ead38bb1 100644 --- a/tests/baselines/reference/thisConditionalInferenceInClassBody.types +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.types @@ -38,3 +38,16 @@ set(new Foo(), 'prop', 'hi'); // <-- typechecks >'prop' : "prop" >'hi' : "hi" +type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; +>InferBecauseWhyNot : InferBecauseWhyNot +>p : P1 + +function f any>(x: Q): InferBecauseWhyNot { +>f : any>(x: Q) => InferBecauseWhyNot +>arg : any +>x : Q + + return x; +>x : Q +} + diff --git a/tests/cases/compiler/thisConditionalInferenceInClassBody.ts b/tests/cases/compiler/thisConditionalInferenceInClassBody.ts index 7b6c1b2ccde37..94eb259372f82 100644 --- a/tests/cases/compiler/thisConditionalInferenceInClassBody.ts +++ b/tests/cases/compiler/thisConditionalInferenceInClassBody.ts @@ -11,4 +11,10 @@ class Foo { } } -set(new Foo(), 'prop', 'hi'); // <-- typechecks \ No newline at end of file +set(new Foo(), 'prop', 'hi'); // <-- typechecks + +type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; + +function f any>(x: Q): InferBecauseWhyNot { + return x; +} From 8577f7ddfc02f47e3c1d3868b779497d2403513b Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 16 Oct 2018 17:19:27 -0700 Subject: [PATCH 3/4] Err on the safe side --- src/compiler/checker.ts | 19 +++++++----- ...ConditionalInferenceInClassBody.errors.txt | 31 +++++++++++++++++++ .../thisConditionalInferenceInClassBody.types | 2 +- 3 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 tests/baselines/reference/thisConditionalInferenceInClassBody.errors.txt diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index cd39634b15cb5..8cb7832126301 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -11964,19 +11964,22 @@ namespace ts { const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None); const instantiatedExtends = instantiateType(root.extendsType, mapper); const checkConstraint = getSimplifiedType(instantiateType(root.checkType, mapper)); - // TODO: Use InferencePriority.NoConstraints for type parameters which are being used contravariantly/in a write-only context - // (Or, ideally, some future InferencePriority.SuperConstraints should we start tracking them!) - // As-is, this is unsound - it can be made stricter by replacing `instantiateType(root.trueType, combinedMapper)` with - // `getintersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)])` which - // would preserve the type-parametery-ness of the check; but such a limitation makes this branch almost useless, as only + // TODO: + // As-is, this is effectively sound, but not particularly useful, thanks to all the types it wrongly rejects - only // conditional types with effectively "independent" inference parameters will end up being assignable via this branch, eg // `type InferBecauseWhyNot = T extends (p: infer P1) => any ? T | P1 : never;` - // contains a union in the `true` branch, and so while we can't confirm assignability to `P1`, we _could_ confirm assignability - // to `T`. + // contains a union in the `true` branch, and so while we can't confirm assignability to `P1`, we can confirm assignability to `T`. + // A lenient version could be made by replacing `getintersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)])` + // with `instantiateType(root.trueType, combinedMapper)` which would skip checking aginst the type-parametery-ness of the check; + // but such a change introduces quite a bit of unsoundness as we stop checking against the type-parameteryness of the `infer` type, + // which in turn prevents us from erroring on, eg, unsafe write-position assignments of the constraint of the type. + // To be correct here, we'd need to track the implied variance of the infer parameters and _infer_ appropriately (in addition to checking appropriately) + // Specifically, we'd need to infer with `InferencePriority.NoConstraint` (or ideally a hypothetical `InferencePriority.SuperConstraint`) for contravariant types, + // but continue using the constraints for covariant ones. inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.AlwaysStrict); const combinedMapper = combineTypeMappers(mapper, context); if (isRelatedTo(checkConstraint, instantiateType(root.extendsType, combinedMapper))) { - if (result = isRelatedTo(source, instantiateType(root.trueType, combinedMapper), reportErrors)) { + if (result = isRelatedTo(source, getIntersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)]), reportErrors)) { errorInfo = saveErrorInfo; return result; } diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.errors.txt b/tests/baselines/reference/thisConditionalInferenceInClassBody.errors.txt new file mode 100644 index 0000000000000..71762e7e8909f --- /dev/null +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.errors.txt @@ -0,0 +1,31 @@ +tests/cases/compiler/thisConditionalInferenceInClassBody.ts(10,27): error TS2345: Argument of type '"hi"' is not assignable to parameter of type 'Unwrap'. + Type '"hi"' is not assignable to type 'string & U'. + Type '"hi"' is not assignable to type 'U'. + + +==== tests/cases/compiler/thisConditionalInferenceInClassBody.ts (1 errors) ==== + type Wrapped = { ___secret: T }; + type Unwrap = T extends Wrapped ? U : T; + + declare function set(obj: T, key: K, value: Unwrap): Unwrap; + + class Foo { + prop: Wrapped; + + method() { + set(this, 'prop', 'hi'); // <-- type error + ~~~~ +!!! error TS2345: Argument of type '"hi"' is not assignable to parameter of type 'Unwrap'. +!!! error TS2345: Type '"hi"' is not assignable to type 'string & U'. +!!! error TS2345: Type '"hi"' is not assignable to type 'U'. + } + } + + set(new Foo(), 'prop', 'hi'); // <-- typechecks + + type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; + + function f any>(x: Q): InferBecauseWhyNot { + return x; + } + \ No newline at end of file diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.types b/tests/baselines/reference/thisConditionalInferenceInClassBody.types index 469f8ead38bb1..b515e3d2c106c 100644 --- a/tests/baselines/reference/thisConditionalInferenceInClassBody.types +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.types @@ -22,7 +22,7 @@ class Foo { >method : () => void set(this, 'prop', 'hi'); // <-- type error ->set(this, 'prop', 'hi') : Unwrap +>set(this, 'prop', 'hi') : any >set : (obj: T, key: K, value: Unwrap) => Unwrap >this : this >'prop' : "prop" From f590103e3c5f8c1b30980edd9cc210b9bd4ce1be Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 16 Oct 2018 19:08:09 -0700 Subject: [PATCH 4/4] Stronger guarantees on edge case handling, stolen tests from #27589 --- src/compiler/checker.ts | 141 ++++++++++++++---- src/compiler/types.ts | 2 + .../reference/conditionalTypes2.errors.txt | 38 ++++- .../baselines/reference/conditionalTypes2.js | 50 +++++++ .../reference/conditionalTypes2.symbols | 92 ++++++++++++ .../reference/conditionalTypes2.types | 77 ++++++++++ ...ConditionalInferenceInClassBody.errors.txt | 19 ++- .../thisConditionalInferenceInClassBody.js | 11 +- ...hisConditionalInferenceInClassBody.symbols | 32 +++- .../thisConditionalInferenceInClassBody.types | 15 +- .../thisConditionalInferenceInClassBody.ts | 8 +- .../types/conditional/conditionalTypes2.ts | 24 +++ 12 files changed, 465 insertions(+), 44 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8cb7832126301..f3264c11e2040 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -662,6 +662,7 @@ namespace ts { ImmediateBaseConstraint, EnumTagType, JSDocTypeReference, + NeverInstantiable, } const enum CheckMode { @@ -4520,6 +4521,8 @@ namespace ts { return !!(target).immediateBaseConstraint; case TypeSystemPropertyName.JSDocTypeReference: return !!getSymbolLinks(target as Symbol).resolvedJSDocType; + case TypeSystemPropertyName.NeverInstantiable: + return typeof (target as Type).instantiableToNever !== "undefined"; } return Debug.assertNever(propertyName); } @@ -11126,6 +11129,12 @@ namespace ts { !t.numberIndexInfo; } + function isKeylessResolvedType(t: ResolvedType) { + return t.properties.length === 0 && + !t.stringIndexInfo && + !t.numberIndexInfo; + } + function isEmptyObjectType(type: Type): boolean { return type.flags & TypeFlags.Object ? isEmptyResolvedType(resolveStructuredTypeMembers(type)) : type.flags & TypeFlags.NonPrimitive ? true : @@ -11134,6 +11143,14 @@ namespace ts { false; } + function isKeylessObjectType(type: Type): boolean { + return type.flags & TypeFlags.Object ? isKeylessResolvedType(resolveStructuredTypeMembers(type)) : + type.flags & TypeFlags.NonPrimitive ? true : + type.flags & TypeFlags.Union ? some((type).types, isKeylessObjectType) : + type.flags & TypeFlags.Intersection ? every((type).types, isKeylessObjectType) : + false; + } + function isEnumTypeRelatedTo(sourceSymbol: Symbol, targetSymbol: Symbol, errorReporter?: ErrorReporter) { if (sourceSymbol === targetSymbol) { return true; @@ -11854,6 +11871,70 @@ namespace ts { return relation === definitelyAssignableRelation ? undefined : getConstraintOfType(type); } + function canBeKeyless(type: Type): boolean { + if (canBeNever(type)) { + return true; // `keyof never` is `never`, ergo `never` counts as keyless even though conceptually it contains every key + } + if (type.flags & TypeFlags.Intersection) { + return every((type as IntersectionType).types, canBeKeyless); + } + if (type.flags & TypeFlags.Union) { + return some((type as UnionType).types, canBeKeyless); + } + if (type.flags & TypeFlags.Conditional) { + return canBeKeyless(getTrueTypeFromConditionalType(type as ConditionalType)) || canBeKeyless(getFalseTypeFromConditionalType(type as ConditionalType)); + } + if (type.flags & TypeFlags.Index) { + return canBeKeyless((type as IndexType).type); + } + if (type.flags & TypeFlags.IndexedAccess) { + return canBeKeyless((type as IndexedAccessType).objectType); + } + if (type.flags & TypeFlags.TypeParameter) { + const constraint = getConstraintOfTypeParameter(type as TypeParameter); + return !constraint || canBeKeyless(constraint); + } + if (type.flags & TypeFlags.Substitution) { + return canBeKeyless((type as SubstitutionType).substitute) && canBeKeyless((type as SubstitutionType).typeVariable); + } + return !!(type.flags & TypeFlags.Unknown) || isKeylessObjectType(type); + } + + function canBeNever(type: Type): boolean { + if (!pushTypeResolution(type, TypeSystemPropertyName.NeverInstantiable)) { + return type.instantiableToNever = true; + } + let result = true; + if (type.flags & TypeFlags.Intersection) { + result = some((type as IntersectionType).types, canBeNever); + } + else if (type.flags & TypeFlags.Union) { + result = every((type as UnionType).types, canBeNever); + } + else if (type.flags & TypeFlags.Conditional) { + result = ((type as ConditionalType).root.isDistributive && canBeNever((type as ConditionalType).checkType)) || + canBeNever(getTrueTypeFromConditionalType(type as ConditionalType)) || + canBeNever(getFalseTypeFromConditionalType(type as ConditionalType)); + } + else if (type.flags & TypeFlags.Index) { + result = canBeKeyless((type as IndexType).type); + } + else if (type.flags & TypeFlags.IndexedAccess) { + result = canBeNever((type as IndexedAccessType).indexType) || canBeNever((type as IndexedAccessType).objectType); + } + else if (type.flags & (TypeFlags.TypeParameter | TypeFlags.Substitution)) { + // If we ever get `super` constraints, adjust this + result = true; + } + else { + result = !!(type.flags & TypeFlags.Never); + } + if (!popTypeResolution()) { + return type.instantiableToNever = true; + } + return type.instantiableToNever = result; + } + function structuredTypeRelatedTo(source: Type, target: Type, reportErrors: boolean, isIntersectionConstituent: boolean): Ternary { const flags = source.flags & target.flags; if (relation === identityRelation && !(flags & TypeFlags.Object)) { @@ -11957,40 +12038,42 @@ namespace ts { } else if (target.flags & TypeFlags.Conditional) { const root = (target as ConditionalType).root; - if (root.inferTypeParameters) { - // If the constraint indicates that the conditional type is always true (but it is stil deferred to allow for, eg, distribution or inference) - // We should perform the instantiation and only check against the true type - const mapper = (target as ConditionalType).mapper; - const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None); - const instantiatedExtends = instantiateType(root.extendsType, mapper); - const checkConstraint = getSimplifiedType(instantiateType(root.checkType, mapper)); - // TODO: - // As-is, this is effectively sound, but not particularly useful, thanks to all the types it wrongly rejects - only - // conditional types with effectively "independent" inference parameters will end up being assignable via this branch, eg - // `type InferBecauseWhyNot = T extends (p: infer P1) => any ? T | P1 : never;` - // contains a union in the `true` branch, and so while we can't confirm assignability to `P1`, we can confirm assignability to `T`. - // A lenient version could be made by replacing `getintersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)])` - // with `instantiateType(root.trueType, combinedMapper)` which would skip checking aginst the type-parametery-ness of the check; - // but such a change introduces quite a bit of unsoundness as we stop checking against the type-parameteryness of the `infer` type, - // which in turn prevents us from erroring on, eg, unsafe write-position assignments of the constraint of the type. - // To be correct here, we'd need to track the implied variance of the infer parameters and _infer_ appropriately (in addition to checking appropriately) - // Specifically, we'd need to infer with `InferencePriority.NoConstraint` (or ideally a hypothetical `InferencePriority.SuperConstraint`) for contravariant types, - // but continue using the constraints for covariant ones. - inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.AlwaysStrict); - const combinedMapper = combineTypeMappers(mapper, context); - if (isRelatedTo(checkConstraint, instantiateType(root.extendsType, combinedMapper))) { - if (result = isRelatedTo(source, getIntersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)]), reportErrors)) { + if (!root.isDistributive || !canBeNever((target as ConditionalType).checkType)) { + if (root.inferTypeParameters) { + // If the constraint indicates that the conditional type is always true (but it is stil deferred to allow for, eg, distribution or inference) + // We should perform the instantiation and only check against the true type + const mapper = (target as ConditionalType).mapper; + const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None); + const instantiatedExtends = instantiateType(root.extendsType, mapper); + const checkConstraint = getSimplifiedType(instantiateType(root.checkType, mapper)); + // TODO: + // As-is, this is effectively sound, but not particularly useful, thanks to all the types it wrongly rejects - only + // conditional types with effectively "independent" inference parameters will end up being assignable via this branch, eg + // `type InferBecauseWhyNot = T extends (p: infer P1) => any ? T | P1 : never;` + // contains a union in the `true` branch, and so while we can't confirm assignability to `P1`, we can confirm assignability to `T`. + // A lenient version could be made by replacing `getintersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)])` + // with `instantiateType(root.trueType, combinedMapper)` which would skip checking aginst the type-parametery-ness of the check; + // but such a change introduces quite a bit of unsoundness as we stop checking against the type-parameteryness of the `infer` type, + // which in turn prevents us from erroring on, eg, unsafe write-position assignments of the constraint of the type. + // To be correct here, we'd need to track the implied variance of the infer parameters and _infer_ appropriately (in addition to checking appropriately) + // Specifically, we'd need to infer with `InferencePriority.NoConstraint` (or ideally a hypothetical `InferencePriority.SuperConstraint`) for contravariant types, + // but continue using the constraints for covariant ones. + inferTypes(context.inferences, checkConstraint, instantiatedExtends, InferencePriority.AlwaysStrict); + const combinedMapper = combineTypeMappers(mapper, context); + if (isRelatedTo(checkConstraint, instantiateType(root.extendsType, combinedMapper))) { + if (result = isRelatedTo(source, getIntersectionType([instantiateType(root.trueType, combinedMapper), instantiateType(root.trueType, mapper)]), reportErrors)) { + errorInfo = saveErrorInfo; + return result; + } + } + } + if (result = isRelatedTo(source, getTrueTypeFromConditionalType(target as ConditionalType))) { + if (result &= isRelatedTo(source, getFalseTypeFromConditionalType(target as ConditionalType))) { errorInfo = saveErrorInfo; return result; } } } - if (result = isRelatedTo(source, getTrueTypeFromConditionalType(target as ConditionalType))) { - if (result = isRelatedTo(source, getFalseTypeFromConditionalType(target as ConditionalType))) { - errorInfo = saveErrorInfo; - return result; - } - } } if (source.flags & TypeFlags.TypeVariable) { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index f2c9238aace41..fc8d108613e08 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3894,6 +3894,8 @@ namespace ts { wildcardInstantiation?: Type; // Instantiation with type parameters mapped to wildcard type /* @internal */ immediateBaseConstraint?: Type; // Immediate base constraint cache + /* @internal */ + instantiableToNever?: boolean; // Flag set by `canBeNever` } /* @internal */ diff --git a/tests/baselines/reference/conditionalTypes2.errors.txt b/tests/baselines/reference/conditionalTypes2.errors.txt index 49a36be96dbfd..b461cf5858fa2 100644 --- a/tests/baselines/reference/conditionalTypes2.errors.txt +++ b/tests/baselines/reference/conditionalTypes2.errors.txt @@ -24,9 +24,13 @@ tests/cases/conformance/types/conditional/conditionalTypes2.ts(74,12): error TS2 tests/cases/conformance/types/conditional/conditionalTypes2.ts(75,12): error TS2345: Argument of type 'Extract2' is not assignable to parameter of type '{ foo: string; bat: string; }'. Type 'T extends Bar ? T : never' is not assignable to type '{ foo: string; bat: string; }'. Type 'Bar & Foo & T' is not assignable to type '{ foo: string; bat: string; }'. +tests/cases/conformance/types/conditional/conditionalTypes2.ts(161,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type '[T] extends [[infer U]] ? U : { b: number; }'. +tests/cases/conformance/types/conditional/conditionalTypes2.ts(163,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive'. +tests/cases/conformance/types/conditional/conditionalTypes2.ts(165,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive'. +tests/cases/conformance/types/conditional/conditionalTypes2.ts(169,11): error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<[T] extends [never] ? { a: number; } : never>'. -==== tests/cases/conformance/types/conditional/conditionalTypes2.ts (7 errors) ==== +==== tests/cases/conformance/types/conditional/conditionalTypes2.ts (11 errors) ==== interface Covariant { foo: T extends string ? T : number; } @@ -206,4 +210,36 @@ tests/cases/conformance/types/conditional/conditionalTypes2.ts(75,12): error TS2 type C2 = T extends object ? { [Q in keyof T]: C2; } : T; + + // #26933 + type Distributive = T extends { a: number } ? { a: number } : { b: number }; + function testAssignabilityToConditionalType() { + const o = { a: 1, b: 2 }; + const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!; + // Simple case: OK + const o1: [T] extends [number] ? { a: number } : { b: number } = o; + // Simple case where source happens to be a conditional type: also OK + const x1: [T] extends [number] + ? ([T] extends [string] ? { y: number } : { a: number }) + : ([T] extends [string] ? { y: number } : { b: number }) + = x; + // Infer type parameters: no good + const o2: [T] extends [[infer U]] ? U : { b: number } = o; + ~~ +!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type '[T] extends [[infer U]] ? U : { b: number; }'. + // Distributive where T might instantiate to never: no good + const o3: Distributive = o; + ~~ +!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive'. + // Distributive where T & string might instantiate to never: also no good + const o4: Distributive = o; + ~~ +!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive'. + // Distributive where {a: T} cannot instantiate to never: OK + const o5: Distributive<{ a: T }> = o; + // Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good + const o6: Distributive<[T] extends [never] ? { a: number } : never> = o; + ~~ +!!! error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'Distributive<[T] extends [never] ? { a: number; } : never>'. + } \ No newline at end of file diff --git a/tests/baselines/reference/conditionalTypes2.js b/tests/baselines/reference/conditionalTypes2.js index 1f95e7eb8d9f2..4bbfc794064a2 100644 --- a/tests/baselines/reference/conditionalTypes2.js +++ b/tests/baselines/reference/conditionalTypes2.js @@ -145,6 +145,30 @@ type B2 = type C2 = T extends object ? { [Q in keyof T]: C2; } : T; + +// #26933 +type Distributive = T extends { a: number } ? { a: number } : { b: number }; +function testAssignabilityToConditionalType() { + const o = { a: 1, b: 2 }; + const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!; + // Simple case: OK + const o1: [T] extends [number] ? { a: number } : { b: number } = o; + // Simple case where source happens to be a conditional type: also OK + const x1: [T] extends [number] + ? ([T] extends [string] ? { y: number } : { a: number }) + : ([T] extends [string] ? { y: number } : { b: number }) + = x; + // Infer type parameters: no good + const o2: [T] extends [[infer U]] ? U : { b: number } = o; + // Distributive where T might instantiate to never: no good + const o3: Distributive = o; + // Distributive where T & string might instantiate to never: also no good + const o4: Distributive = o; + // Distributive where {a: T} cannot instantiate to never: OK + const o5: Distributive<{ a: T }> = o; + // Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good + const o6: Distributive<[T] extends [never] ? { a: number } : never> = o; +} //// [conditionalTypes2.js] @@ -222,6 +246,24 @@ function foo(value) { toString2(value); } } +function testAssignabilityToConditionalType() { + var o = { a: 1, b: 2 }; + var x = undefined; + // Simple case: OK + var o1 = o; + // Simple case where source happens to be a conditional type: also OK + var x1 = x; + // Infer type parameters: no good + var o2 = o; + // Distributive where T might instantiate to never: no good + var o3 = o; + // Distributive where T & string might instantiate to never: also no good + var o4 = o; + // Distributive where {a: T} cannot instantiate to never: OK + var o5 = o; + // Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good + var o6 = o; +} //// [conditionalTypes2.d.ts] @@ -304,3 +346,11 @@ declare type B2 = T extends object ? T extends any[] ? T : { declare type C2 = T extends object ? { [Q in keyof T]: C2; } : T; +declare type Distributive = T extends { + a: number; +} ? { + a: number; +} : { + b: number; +}; +declare function testAssignabilityToConditionalType(): void; diff --git a/tests/baselines/reference/conditionalTypes2.symbols b/tests/baselines/reference/conditionalTypes2.symbols index c52179e81b01c..02720826bb0ef 100644 --- a/tests/baselines/reference/conditionalTypes2.symbols +++ b/tests/baselines/reference/conditionalTypes2.symbols @@ -551,3 +551,95 @@ type C2 = >E : Symbol(E, Decl(conditionalTypes2.ts, 144, 13)) >T : Symbol(T, Decl(conditionalTypes2.ts, 144, 8)) +// #26933 +type Distributive = T extends { a: number } ? { a: number } : { b: number }; +>Distributive : Symbol(Distributive, Decl(conditionalTypes2.ts, 145, 63)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 148, 18)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 148, 18)) +>a : Symbol(a, Decl(conditionalTypes2.ts, 148, 34)) +>a : Symbol(a, Decl(conditionalTypes2.ts, 148, 50)) +>b : Symbol(b, Decl(conditionalTypes2.ts, 148, 66)) + +function testAssignabilityToConditionalType() { +>testAssignabilityToConditionalType : Symbol(testAssignabilityToConditionalType, Decl(conditionalTypes2.ts, 148, 79)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) + + const o = { a: 1, b: 2 }; +>o : Symbol(o, Decl(conditionalTypes2.ts, 150, 9)) +>a : Symbol(a, Decl(conditionalTypes2.ts, 150, 15)) +>b : Symbol(b, Decl(conditionalTypes2.ts, 150, 21)) + + const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!; +>x : Symbol(x, Decl(conditionalTypes2.ts, 151, 9)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>y : Symbol(y, Decl(conditionalTypes2.ts, 151, 37)) +>a : Symbol(a, Decl(conditionalTypes2.ts, 151, 53)) +>b : Symbol(b, Decl(conditionalTypes2.ts, 151, 64)) +>undefined : Symbol(undefined) + + // Simple case: OK + const o1: [T] extends [number] ? { a: number } : { b: number } = o; +>o1 : Symbol(o1, Decl(conditionalTypes2.ts, 153, 9)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>a : Symbol(a, Decl(conditionalTypes2.ts, 153, 38)) +>b : Symbol(b, Decl(conditionalTypes2.ts, 153, 54)) +>o : Symbol(o, Decl(conditionalTypes2.ts, 150, 9)) + + // Simple case where source happens to be a conditional type: also OK + const x1: [T] extends [number] +>x1 : Symbol(x1, Decl(conditionalTypes2.ts, 155, 9)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) + + ? ([T] extends [string] ? { y: number } : { a: number }) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>y : Symbol(y, Decl(conditionalTypes2.ts, 156, 35)) +>a : Symbol(a, Decl(conditionalTypes2.ts, 156, 51)) + + : ([T] extends [string] ? { y: number } : { b: number }) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>y : Symbol(y, Decl(conditionalTypes2.ts, 157, 35)) +>b : Symbol(b, Decl(conditionalTypes2.ts, 157, 51)) + + = x; +>x : Symbol(x, Decl(conditionalTypes2.ts, 151, 9)) + + // Infer type parameters: no good + const o2: [T] extends [[infer U]] ? U : { b: number } = o; +>o2 : Symbol(o2, Decl(conditionalTypes2.ts, 160, 9)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>U : Symbol(U, Decl(conditionalTypes2.ts, 160, 33)) +>U : Symbol(U, Decl(conditionalTypes2.ts, 160, 33)) +>b : Symbol(b, Decl(conditionalTypes2.ts, 160, 45)) +>o : Symbol(o, Decl(conditionalTypes2.ts, 150, 9)) + + // Distributive where T might instantiate to never: no good + const o3: Distributive = o; +>o3 : Symbol(o3, Decl(conditionalTypes2.ts, 162, 9)) +>Distributive : Symbol(Distributive, Decl(conditionalTypes2.ts, 145, 63)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>o : Symbol(o, Decl(conditionalTypes2.ts, 150, 9)) + + // Distributive where T & string might instantiate to never: also no good + const o4: Distributive = o; +>o4 : Symbol(o4, Decl(conditionalTypes2.ts, 164, 9)) +>Distributive : Symbol(Distributive, Decl(conditionalTypes2.ts, 145, 63)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>o : Symbol(o, Decl(conditionalTypes2.ts, 150, 9)) + + // Distributive where {a: T} cannot instantiate to never: OK + const o5: Distributive<{ a: T }> = o; +>o5 : Symbol(o5, Decl(conditionalTypes2.ts, 166, 9)) +>Distributive : Symbol(Distributive, Decl(conditionalTypes2.ts, 145, 63)) +>a : Symbol(a, Decl(conditionalTypes2.ts, 166, 28)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>o : Symbol(o, Decl(conditionalTypes2.ts, 150, 9)) + + // Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good + const o6: Distributive<[T] extends [never] ? { a: number } : never> = o; +>o6 : Symbol(o6, Decl(conditionalTypes2.ts, 168, 9)) +>Distributive : Symbol(Distributive, Decl(conditionalTypes2.ts, 145, 63)) +>T : Symbol(T, Decl(conditionalTypes2.ts, 149, 44)) +>a : Symbol(a, Decl(conditionalTypes2.ts, 168, 50)) +>o : Symbol(o, Decl(conditionalTypes2.ts, 150, 9)) +} + diff --git a/tests/baselines/reference/conditionalTypes2.types b/tests/baselines/reference/conditionalTypes2.types index b1fbfc1f4834d..58d29a8c82282 100644 --- a/tests/baselines/reference/conditionalTypes2.types +++ b/tests/baselines/reference/conditionalTypes2.types @@ -341,3 +341,80 @@ type C2 = T extends object ? { [Q in keyof T]: C2; } : T; +// #26933 +type Distributive = T extends { a: number } ? { a: number } : { b: number }; +>Distributive : Distributive +>a : number +>a : number +>b : number + +function testAssignabilityToConditionalType() { +>testAssignabilityToConditionalType : () => void + + const o = { a: 1, b: 2 }; +>o : { a: number; b: number; } +>{ a: 1, b: 2 } : { a: number; b: number; } +>a : number +>1 : 1 +>b : number +>2 : 2 + + const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!; +>x : [T] extends [string] ? { y: number; } : { a: number; b: number; } +>y : number +>a : number +>b : number +>undefined! : never +>undefined : undefined + + // Simple case: OK + const o1: [T] extends [number] ? { a: number } : { b: number } = o; +>o1 : [T] extends [number] ? { a: number; } : { b: number; } +>a : number +>b : number +>o : { a: number; b: number; } + + // Simple case where source happens to be a conditional type: also OK + const x1: [T] extends [number] +>x1 : [T] extends [number] ? [T] extends [string] ? { y: number; } : { a: number; } : [T] extends [string] ? { y: number; } : { b: number; } + + ? ([T] extends [string] ? { y: number } : { a: number }) +>y : number +>a : number + + : ([T] extends [string] ? { y: number } : { b: number }) +>y : number +>b : number + + = x; +>x : [T] extends [string] ? { y: number; } : { a: number; b: number; } + + // Infer type parameters: no good + const o2: [T] extends [[infer U]] ? U : { b: number } = o; +>o2 : [T] extends [[infer U]] ? U : { b: number; } +>b : number +>o : { a: number; b: number; } + + // Distributive where T might instantiate to never: no good + const o3: Distributive = o; +>o3 : Distributive +>o : { a: number; b: number; } + + // Distributive where T & string might instantiate to never: also no good + const o4: Distributive = o; +>o4 : Distributive +>o : { a: number; b: number; } + + // Distributive where {a: T} cannot instantiate to never: OK + const o5: Distributive<{ a: T }> = o; +>o5 : Distributive<{ a: T; }> +>a : T +>o : { a: number; b: number; } + + // Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good + const o6: Distributive<[T] extends [never] ? { a: number } : never> = o; +>o6 : Distributive<[T] extends [never] ? { a: number; } : never> +>a : number +>o : { a: number; b: number; } +} + diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.errors.txt b/tests/baselines/reference/thisConditionalInferenceInClassBody.errors.txt index 71762e7e8909f..0f0602a9a8a29 100644 --- a/tests/baselines/reference/thisConditionalInferenceInClassBody.errors.txt +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.errors.txt @@ -1,9 +1,9 @@ tests/cases/compiler/thisConditionalInferenceInClassBody.ts(10,27): error TS2345: Argument of type '"hi"' is not assignable to parameter of type 'Unwrap'. - Type '"hi"' is not assignable to type 'string & U'. - Type '"hi"' is not assignable to type 'U'. +tests/cases/compiler/thisConditionalInferenceInClassBody.ts(25,5): error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNotDistributive'. + Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNotDistributive'. -==== tests/cases/compiler/thisConditionalInferenceInClassBody.ts (1 errors) ==== +==== tests/cases/compiler/thisConditionalInferenceInClassBody.ts (2 errors) ==== type Wrapped = { ___secret: T }; type Unwrap = T extends Wrapped ? U : T; @@ -16,16 +16,23 @@ tests/cases/compiler/thisConditionalInferenceInClassBody.ts(10,27): error TS2345 set(this, 'prop', 'hi'); // <-- type error ~~~~ !!! error TS2345: Argument of type '"hi"' is not assignable to parameter of type 'Unwrap'. -!!! error TS2345: Type '"hi"' is not assignable to type 'string & U'. -!!! error TS2345: Type '"hi"' is not assignable to type 'U'. } } set(new Foo(), 'prop', 'hi'); // <-- typechecks - type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; + type InferBecauseWhyNot = [T] extends [(p: infer P1) => any] ? P1 | T : never; function f any>(x: Q): InferBecauseWhyNot { return x; } + + type InferBecauseWhyNotDistributive = T extends (p: infer P1) => any ? P1 | T : never; + + function f2 any>(x: Q): InferBecauseWhyNotDistributive { + return x; // should fail, as when Q = never, InferBecauseWhyNotDistributive = never, + ~~~~~~~~~ +!!! error TS2322: Type 'Q' is not assignable to type 'InferBecauseWhyNotDistributive'. +!!! error TS2322: Type '(arg: any) => any' is not assignable to type 'InferBecauseWhyNotDistributive'. + } \ No newline at end of file diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.js b/tests/baselines/reference/thisConditionalInferenceInClassBody.js index aa800d2f85b4d..884720810ad3c 100644 --- a/tests/baselines/reference/thisConditionalInferenceInClassBody.js +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.js @@ -14,11 +14,17 @@ class Foo { set(new Foo(), 'prop', 'hi'); // <-- typechecks -type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; +type InferBecauseWhyNot = [T] extends [(p: infer P1) => any] ? P1 | T : never; function f any>(x: Q): InferBecauseWhyNot { return x; } + +type InferBecauseWhyNotDistributive = T extends (p: infer P1) => any ? P1 | T : never; + +function f2 any>(x: Q): InferBecauseWhyNotDistributive { + return x; // should fail, as when Q = never, InferBecauseWhyNotDistributive = never, +} //// [thisConditionalInferenceInClassBody.js] @@ -34,3 +40,6 @@ set(new Foo(), 'prop', 'hi'); // <-- typechecks function f(x) { return x; } +function f2(x) { + return x; // should fail, as when Q = never, InferBecauseWhyNotDistributive = never, +} diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols b/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols index bab2788cbae12..c51ce8678309c 100644 --- a/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.symbols @@ -51,17 +51,17 @@ set(new Foo(), 'prop', 'hi'); // <-- typechecks >set : Symbol(set, Decl(thisConditionalInferenceInClassBody.ts, 1, 52)) >Foo : Symbol(Foo, Decl(thisConditionalInferenceInClassBody.ts, 3, 94)) -type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; +type InferBecauseWhyNot = [T] extends [(p: infer P1) => any] ? P1 | T : never; >InferBecauseWhyNot : Symbol(InferBecauseWhyNot, Decl(thisConditionalInferenceInClassBody.ts, 13, 29)) >T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 15, 24)) >T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 15, 24)) ->p : Symbol(p, Decl(thisConditionalInferenceInClassBody.ts, 15, 40)) ->P1 : Symbol(P1, Decl(thisConditionalInferenceInClassBody.ts, 15, 48)) ->P1 : Symbol(P1, Decl(thisConditionalInferenceInClassBody.ts, 15, 48)) +>p : Symbol(p, Decl(thisConditionalInferenceInClassBody.ts, 15, 43)) +>P1 : Symbol(P1, Decl(thisConditionalInferenceInClassBody.ts, 15, 51)) +>P1 : Symbol(P1, Decl(thisConditionalInferenceInClassBody.ts, 15, 51)) >T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 15, 24)) function f any>(x: Q): InferBecauseWhyNot { ->f : Symbol(f, Decl(thisConditionalInferenceInClassBody.ts, 15, 77)) +>f : Symbol(f, Decl(thisConditionalInferenceInClassBody.ts, 15, 81)) >Q : Symbol(Q, Decl(thisConditionalInferenceInClassBody.ts, 17, 11)) >arg : Symbol(arg, Decl(thisConditionalInferenceInClassBody.ts, 17, 22)) >x : Symbol(x, Decl(thisConditionalInferenceInClassBody.ts, 17, 40)) @@ -73,3 +73,25 @@ function f any>(x: Q): InferBecauseWhyNot { >x : Symbol(x, Decl(thisConditionalInferenceInClassBody.ts, 17, 40)) } +type InferBecauseWhyNotDistributive = T extends (p: infer P1) => any ? P1 | T : never; +>InferBecauseWhyNotDistributive : Symbol(InferBecauseWhyNotDistributive, Decl(thisConditionalInferenceInClassBody.ts, 19, 1)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 21, 36)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 21, 36)) +>p : Symbol(p, Decl(thisConditionalInferenceInClassBody.ts, 21, 52)) +>P1 : Symbol(P1, Decl(thisConditionalInferenceInClassBody.ts, 21, 60)) +>P1 : Symbol(P1, Decl(thisConditionalInferenceInClassBody.ts, 21, 60)) +>T : Symbol(T, Decl(thisConditionalInferenceInClassBody.ts, 21, 36)) + +function f2 any>(x: Q): InferBecauseWhyNotDistributive { +>f2 : Symbol(f2, Decl(thisConditionalInferenceInClassBody.ts, 21, 89)) +>Q : Symbol(Q, Decl(thisConditionalInferenceInClassBody.ts, 23, 12)) +>arg : Symbol(arg, Decl(thisConditionalInferenceInClassBody.ts, 23, 23)) +>x : Symbol(x, Decl(thisConditionalInferenceInClassBody.ts, 23, 41)) +>Q : Symbol(Q, Decl(thisConditionalInferenceInClassBody.ts, 23, 12)) +>InferBecauseWhyNotDistributive : Symbol(InferBecauseWhyNotDistributive, Decl(thisConditionalInferenceInClassBody.ts, 19, 1)) +>Q : Symbol(Q, Decl(thisConditionalInferenceInClassBody.ts, 23, 12)) + + return x; // should fail, as when Q = never, InferBecauseWhyNotDistributive = never, +>x : Symbol(x, Decl(thisConditionalInferenceInClassBody.ts, 23, 41)) +} + diff --git a/tests/baselines/reference/thisConditionalInferenceInClassBody.types b/tests/baselines/reference/thisConditionalInferenceInClassBody.types index b515e3d2c106c..1228c44186363 100644 --- a/tests/baselines/reference/thisConditionalInferenceInClassBody.types +++ b/tests/baselines/reference/thisConditionalInferenceInClassBody.types @@ -38,7 +38,7 @@ set(new Foo(), 'prop', 'hi'); // <-- typechecks >'prop' : "prop" >'hi' : "hi" -type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; +type InferBecauseWhyNot = [T] extends [(p: infer P1) => any] ? P1 | T : never; >InferBecauseWhyNot : InferBecauseWhyNot >p : P1 @@ -51,3 +51,16 @@ function f any>(x: Q): InferBecauseWhyNot { >x : Q } +type InferBecauseWhyNotDistributive = T extends (p: infer P1) => any ? P1 | T : never; +>InferBecauseWhyNotDistributive : InferBecauseWhyNotDistributive +>p : P1 + +function f2 any>(x: Q): InferBecauseWhyNotDistributive { +>f2 : any>(x: Q) => InferBecauseWhyNotDistributive +>arg : any +>x : Q + + return x; // should fail, as when Q = never, InferBecauseWhyNotDistributive = never, +>x : Q +} + diff --git a/tests/cases/compiler/thisConditionalInferenceInClassBody.ts b/tests/cases/compiler/thisConditionalInferenceInClassBody.ts index 94eb259372f82..9b5a216a7193f 100644 --- a/tests/cases/compiler/thisConditionalInferenceInClassBody.ts +++ b/tests/cases/compiler/thisConditionalInferenceInClassBody.ts @@ -13,8 +13,14 @@ class Foo { set(new Foo(), 'prop', 'hi'); // <-- typechecks -type InferBecauseWhyNot = T extends (p: infer P1) => any ? P1 | T : never; +type InferBecauseWhyNot = [T] extends [(p: infer P1) => any] ? P1 | T : never; function f any>(x: Q): InferBecauseWhyNot { return x; } + +type InferBecauseWhyNotDistributive = T extends (p: infer P1) => any ? P1 | T : never; + +function f2 any>(x: Q): InferBecauseWhyNotDistributive { + return x; // should fail, as when Q = never, InferBecauseWhyNotDistributive = never, +} diff --git a/tests/cases/conformance/types/conditional/conditionalTypes2.ts b/tests/cases/conformance/types/conditional/conditionalTypes2.ts index be75894546944..6a7c1042acf8f 100644 --- a/tests/cases/conformance/types/conditional/conditionalTypes2.ts +++ b/tests/cases/conformance/types/conditional/conditionalTypes2.ts @@ -147,3 +147,27 @@ type B2 = type C2 = T extends object ? { [Q in keyof T]: C2; } : T; + +// #26933 +type Distributive = T extends { a: number } ? { a: number } : { b: number }; +function testAssignabilityToConditionalType() { + const o = { a: 1, b: 2 }; + const x: [T] extends [string] ? { y: number } : { a: number, b: number } = undefined!; + // Simple case: OK + const o1: [T] extends [number] ? { a: number } : { b: number } = o; + // Simple case where source happens to be a conditional type: also OK + const x1: [T] extends [number] + ? ([T] extends [string] ? { y: number } : { a: number }) + : ([T] extends [string] ? { y: number } : { b: number }) + = x; + // Infer type parameters: no good + const o2: [T] extends [[infer U]] ? U : { b: number } = o; + // Distributive where T might instantiate to never: no good + const o3: Distributive = o; + // Distributive where T & string might instantiate to never: also no good + const o4: Distributive = o; + // Distributive where {a: T} cannot instantiate to never: OK + const o5: Distributive<{ a: T }> = o; + // Distributive where check type is a conditional which returns a non-never type upon instantiation with `never` but can still return never otherwise: no good + const o6: Distributive<[T] extends [never] ? { a: number } : never> = o; +}