From 0cc5c7bae9dfc0bee5693285c58e3962d7591784 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Thu, 13 May 2021 13:48:41 -0700 Subject: [PATCH] Dont allow generic narrowing when contextually typed by a binding pattern --- src/compiler/checker.ts | 6 +- .../genericObjectSpreadResultInSwitch.js | 67 +++++++++++++++++ .../genericObjectSpreadResultInSwitch.symbols | 70 ++++++++++++++++++ .../genericObjectSpreadResultInSwitch.types | 73 +++++++++++++++++++ .../reference/restInvalidArgumentType.types | 2 +- .../genericObjectSpreadResultInSwitch.ts | 33 +++++++++ 6 files changed, 247 insertions(+), 4 deletions(-) create mode 100644 tests/baselines/reference/genericObjectSpreadResultInSwitch.js create mode 100644 tests/baselines/reference/genericObjectSpreadResultInSwitch.symbols create mode 100644 tests/baselines/reference/genericObjectSpreadResultInSwitch.types create mode 100644 tests/cases/compiler/genericObjectSpreadResultInSwitch.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index b2036d24a3138..576c055762f85 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -24124,12 +24124,12 @@ namespace ts { return !!(type.flags & TypeFlags.Instantiable || type.flags & TypeFlags.UnionOrIntersection && some((type).types, containsGenericType)); } - function hasContextualTypeWithNoGenericTypes(node: Node) { + function hasNonBindingPatternContextualTypeWithNoGenericTypes(node: Node) { // Computing the contextual type for a child of a JSX element involves resolving the type of the // element's tag name, so we exclude that here to avoid circularities. const contextualType = (isIdentifier(node) || isPropertyAccessExpression(node) || isElementAccessExpression(node)) && !((isJsxOpeningElement(node.parent) || isJsxSelfClosingElement(node.parent)) && node.parent.tagName === node) && - getContextualType(node); + getContextualType(node, ContextFlags.SkipBindingPatterns); return contextualType && !someType(contextualType, containsGenericType); } @@ -24143,7 +24143,7 @@ namespace ts { // 'string | undefined' to give control flow analysis the opportunity to narrow to type 'string'. const substituteConstraints = !(checkMode && checkMode & CheckMode.Inferential) && someType(type, isGenericTypeWithUnionConstraint) && - (isConstraintPosition(reference) || hasContextualTypeWithNoGenericTypes(reference)); + (isConstraintPosition(reference) || hasNonBindingPatternContextualTypeWithNoGenericTypes(reference)); return substituteConstraints ? mapType(type, t => t.flags & TypeFlags.Instantiable ? getBaseConstraintOrType(t) : t) : type; } diff --git a/tests/baselines/reference/genericObjectSpreadResultInSwitch.js b/tests/baselines/reference/genericObjectSpreadResultInSwitch.js new file mode 100644 index 0000000000000..539805770d596 --- /dev/null +++ b/tests/baselines/reference/genericObjectSpreadResultInSwitch.js @@ -0,0 +1,67 @@ +//// [genericObjectSpreadResultInSwitch.ts] +type Params = { + foo: string; +} & ({ tag: 'a'; type: number } | { tag: 'b'; type: string }); + +const getType =

(params: P) => { + const { + // Omit + foo, + + ...rest + } = params; + + return rest; +}; + +declare const params: Params; + +switch (params.tag) { + case 'a': { + // TS 4.2: number + // TS 4.3: string | number + const result = getType(params).type; + + break; + } + case 'b': { + // TS 4.2: string + // TS 4.3: string | number + const result = getType(params).type; + + break; + } +} + +//// [genericObjectSpreadResultInSwitch.js] +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +var getType = function (params) { + var + // Omit + foo = params.foo, rest = __rest(params, ["foo"]); + return rest; +}; +switch (params.tag) { + case 'a': { + // TS 4.2: number + // TS 4.3: string | number + var result = getType(params).type; + break; + } + case 'b': { + // TS 4.2: string + // TS 4.3: string | number + var result = getType(params).type; + break; + } +} diff --git a/tests/baselines/reference/genericObjectSpreadResultInSwitch.symbols b/tests/baselines/reference/genericObjectSpreadResultInSwitch.symbols new file mode 100644 index 0000000000000..56868aee4e94f --- /dev/null +++ b/tests/baselines/reference/genericObjectSpreadResultInSwitch.symbols @@ -0,0 +1,70 @@ +=== tests/cases/compiler/genericObjectSpreadResultInSwitch.ts === +type Params = { +>Params : Symbol(Params, Decl(genericObjectSpreadResultInSwitch.ts, 0, 0)) + + foo: string; +>foo : Symbol(foo, Decl(genericObjectSpreadResultInSwitch.ts, 0, 15)) + +} & ({ tag: 'a'; type: number } | { tag: 'b'; type: string }); +>tag : Symbol(tag, Decl(genericObjectSpreadResultInSwitch.ts, 2, 6)) +>type : Symbol(type, Decl(genericObjectSpreadResultInSwitch.ts, 2, 16)) +>tag : Symbol(tag, Decl(genericObjectSpreadResultInSwitch.ts, 2, 35)) +>type : Symbol(type, Decl(genericObjectSpreadResultInSwitch.ts, 2, 45)) + +const getType =

(params: P) => { +>getType : Symbol(getType, Decl(genericObjectSpreadResultInSwitch.ts, 4, 5)) +>P : Symbol(P, Decl(genericObjectSpreadResultInSwitch.ts, 4, 17)) +>Params : Symbol(Params, Decl(genericObjectSpreadResultInSwitch.ts, 0, 0)) +>params : Symbol(params, Decl(genericObjectSpreadResultInSwitch.ts, 4, 35)) +>P : Symbol(P, Decl(genericObjectSpreadResultInSwitch.ts, 4, 17)) + + const { + // Omit + foo, +>foo : Symbol(foo, Decl(genericObjectSpreadResultInSwitch.ts, 5, 11)) + + ...rest +>rest : Symbol(rest, Decl(genericObjectSpreadResultInSwitch.ts, 7, 12)) + + } = params; +>params : Symbol(params, Decl(genericObjectSpreadResultInSwitch.ts, 4, 35)) + + return rest; +>rest : Symbol(rest, Decl(genericObjectSpreadResultInSwitch.ts, 7, 12)) + +}; + +declare const params: Params; +>params : Symbol(params, Decl(genericObjectSpreadResultInSwitch.ts, 15, 13)) +>Params : Symbol(Params, Decl(genericObjectSpreadResultInSwitch.ts, 0, 0)) + +switch (params.tag) { +>params.tag : Symbol(tag, Decl(genericObjectSpreadResultInSwitch.ts, 2, 6), Decl(genericObjectSpreadResultInSwitch.ts, 2, 35)) +>params : Symbol(params, Decl(genericObjectSpreadResultInSwitch.ts, 15, 13)) +>tag : Symbol(tag, Decl(genericObjectSpreadResultInSwitch.ts, 2, 6), Decl(genericObjectSpreadResultInSwitch.ts, 2, 35)) + + case 'a': { + // TS 4.2: number + // TS 4.3: string | number + const result = getType(params).type; +>result : Symbol(result, Decl(genericObjectSpreadResultInSwitch.ts, 21, 13)) +>getType(params).type : Symbol(type, Decl(genericObjectSpreadResultInSwitch.ts, 2, 16)) +>getType : Symbol(getType, Decl(genericObjectSpreadResultInSwitch.ts, 4, 5)) +>params : Symbol(params, Decl(genericObjectSpreadResultInSwitch.ts, 15, 13)) +>type : Symbol(type, Decl(genericObjectSpreadResultInSwitch.ts, 2, 16)) + + break; + } + case 'b': { + // TS 4.2: string + // TS 4.3: string | number + const result = getType(params).type; +>result : Symbol(result, Decl(genericObjectSpreadResultInSwitch.ts, 28, 13)) +>getType(params).type : Symbol(type, Decl(genericObjectSpreadResultInSwitch.ts, 2, 45)) +>getType : Symbol(getType, Decl(genericObjectSpreadResultInSwitch.ts, 4, 5)) +>params : Symbol(params, Decl(genericObjectSpreadResultInSwitch.ts, 15, 13)) +>type : Symbol(type, Decl(genericObjectSpreadResultInSwitch.ts, 2, 45)) + + break; + } +} diff --git a/tests/baselines/reference/genericObjectSpreadResultInSwitch.types b/tests/baselines/reference/genericObjectSpreadResultInSwitch.types new file mode 100644 index 0000000000000..33f485c465bd3 --- /dev/null +++ b/tests/baselines/reference/genericObjectSpreadResultInSwitch.types @@ -0,0 +1,73 @@ +=== tests/cases/compiler/genericObjectSpreadResultInSwitch.ts === +type Params = { +>Params : Params + + foo: string; +>foo : string + +} & ({ tag: 'a'; type: number } | { tag: 'b'; type: string }); +>tag : "a" +>type : number +>tag : "b" +>type : string + +const getType =

(params: P) => { +>getType :

(params: P) => Omit +>

(params: P) => { const { // Omit foo, ...rest } = params; return rest;} :

(params: P) => Omit +>params : P + + const { + // Omit + foo, +>foo : string + + ...rest +>rest : Omit + + } = params; +>params : P + + return rest; +>rest : Omit + +}; + +declare const params: Params; +>params : Params + +switch (params.tag) { +>params.tag : "a" | "b" +>params : Params +>tag : "a" | "b" + + case 'a': { +>'a' : "a" + + // TS 4.2: number + // TS 4.3: string | number + const result = getType(params).type; +>result : number +>getType(params).type : number +>getType(params) : Omit<{ foo: string; } & { tag: "a"; type: number; }, "foo"> +>getType :

(params: P) => Omit +>params : { foo: string; } & { tag: "a"; type: number; } +>type : number + + break; + } + case 'b': { +>'b' : "b" + + // TS 4.2: string + // TS 4.3: string | number + const result = getType(params).type; +>result : string +>getType(params).type : string +>getType(params) : Omit<{ foo: string; } & { tag: "b"; type: string; }, "foo"> +>getType :

(params: P) => Omit +>params : { foo: string; } & { tag: "b"; type: string; } +>type : string + + break; + } +} diff --git a/tests/baselines/reference/restInvalidArgumentType.types b/tests/baselines/reference/restInvalidArgumentType.types index 119b0394726da..0e44cdfe95ccb 100644 --- a/tests/baselines/reference/restInvalidArgumentType.types +++ b/tests/baselines/reference/restInvalidArgumentType.types @@ -84,7 +84,7 @@ function f(p1: T, p2: T[]) { var {...r5} = k; // Error, index >r5 : any ->k : string | number | symbol +>k : keyof T var {...r6} = mapped_generic; // Error, generic mapped object type >r6 : { [P in keyof T]: T[P]; } diff --git a/tests/cases/compiler/genericObjectSpreadResultInSwitch.ts b/tests/cases/compiler/genericObjectSpreadResultInSwitch.ts new file mode 100644 index 0000000000000..51a4a33eb81f9 --- /dev/null +++ b/tests/cases/compiler/genericObjectSpreadResultInSwitch.ts @@ -0,0 +1,33 @@ +type Params = { + foo: string; +} & ({ tag: 'a'; type: number } | { tag: 'b'; type: string }); + +const getType =

(params: P) => { + const { + // Omit + foo, + + ...rest + } = params; + + return rest; +}; + +declare const params: Params; + +switch (params.tag) { + case 'a': { + // TS 4.2: number + // TS 4.3: string | number + const result = getType(params).type; + + break; + } + case 'b': { + // TS 4.2: string + // TS 4.3: string | number + const result = getType(params).type; + + break; + } +} \ No newline at end of file