diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 1cebd4693885b..7f14744f440a5 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -7800,9 +7800,23 @@ namespace ts { } function filterType(type: Type, f: (t: Type) => boolean): Type { - return type.flags & TypeFlags.Union ? - getUnionType(filter((type).types, f)) : - f(type) ? type : neverType; + if (type.flags & TypeFlags.Intersection) { + const filteredSubtypes: Type[] = []; + for (const subType of ( type).types) { + const filtered = filterType(subType, f); + if (filtered === neverType) { + return neverType; + } + filteredSubtypes.push(filtered); + } + return getIntersectionType(filteredSubtypes); + } + if (type.flags & TypeFlags.Union) { + return getUnionType( + map((type).types, t => filterType(t, f)) + ); + } + return f(type) ? type : neverType; } function getFlowTypeOfReference(reference: Node, declaredType: Type, assumeInitialized: boolean, includeOuterFunctions: boolean) { @@ -8095,7 +8109,7 @@ namespace ts { } const propName = propAccess.name.text; const propType = getTypeOfPropertyOfType(type, propName); - if (!propType || !isStringLiteralUnionType(propType)) { + if (!propType || !(isStringLiteralUnionType(propType) || (type.flags & TypeFlags.UnionOrIntersection))) { return type; } const discriminantType = value.kind === SyntaxKind.StringLiteral ? getStringLiteralTypeForText((value).text) : checkExpression(value); @@ -8106,7 +8120,10 @@ namespace ts { assumeTrue = !assumeTrue; } if (assumeTrue) { - return filterType(type, t => areTypesComparable(getTypeOfPropertyOfType(t, propName), discriminantType)); + return filterType(type, t => { + const propType = getTypeOfPropertyOfType(t, propName); + return !propType || areTypesComparable(propType, discriminantType); + }); } if (discriminantType.flags & TypeFlags.StringLiteral) { return filterType(type, t => getTypeOfPropertyOfType(t, propName) !== discriminantType); @@ -8121,7 +8138,7 @@ namespace ts { } const propName = (switchStatement.expression).name.text; const propType = getTypeOfPropertyOfType(type, propName); - if (!propType || !isStringLiteralUnionType(propType)) { + if (!propType || !(isStringLiteralUnionType(propType) || (type.flags & TypeFlags.UnionOrIntersection))) { return type; } const switchTypes = getSwitchClauseTypes(switchStatement); @@ -8132,7 +8149,10 @@ namespace ts { const hasDefaultClause = clauseStart === clauseEnd || contains(clauseTypes, undefined); const caseTypes = hasDefaultClause ? filter(clauseTypes, t => !!t) : clauseTypes; const discriminantType = caseTypes.length ? getUnionType(caseTypes) : undefined; - const caseType = discriminantType && filterType(type, t => isTypeComparableTo(discriminantType, getTypeOfPropertyOfType(t, propName))); + const caseType = discriminantType && filterType(type, t => { + const propType = getTypeOfPropertyOfType(t, propName); + return !propType || isTypeComparableTo(discriminantType, propType); + }); if (!hasDefaultClause) { return caseType; } diff --git a/src/services/formatting/smartIndenter.ts b/src/services/formatting/smartIndenter.ts index bf6760d9ec5c1..3769127784e83 100644 --- a/src/services/formatting/smartIndenter.ts +++ b/src/services/formatting/smartIndenter.ts @@ -350,7 +350,8 @@ namespace ts.formatting { return Value.Unknown; - function getStartingExpression(node: PropertyAccessExpression | CallExpression | ElementAccessExpression) { + // inferred type is (correctly) `never` as all known node kinds are handled, thus resulting in an infinite loop + function getStartingExpression(node: PropertyAccessExpression | CallExpression | ElementAccessExpression): LeftHandSideExpression { while (true) { switch (node.kind) { case SyntaxKind.CallExpression: diff --git a/tests/baselines/reference/discriminatedUnionTypesWithIntersection.errors.txt b/tests/baselines/reference/discriminatedUnionTypesWithIntersection.errors.txt new file mode 100644 index 0000000000000..c91efa38d6eb1 --- /dev/null +++ b/tests/baselines/reference/discriminatedUnionTypesWithIntersection.errors.txt @@ -0,0 +1,161 @@ +tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts(64,18): error TS2339: Property 'str' does not exist on type 'never'. +tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts(75,26): error TS2339: Property 'str' does not exist on type 'never'. +tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts(82,27): error TS2339: Property 'num' does not exist on type 'never'. +tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts(93,31): error TS2339: Property 'num' does not exist on type 'never'. +tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts(97,26): error TS2339: Property 'str' does not exist on type 'never'. + + +==== tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts (5 errors) ==== + type WithX = { + x: string; + } + + type BoxedValue = { kind: 'int', num: number } + | { kind: 'string', str: string } + + type BoxIntersection = BoxedValue & WithX + + type BoxInline = { kind: 'int', num: number } & WithX + | { kind: 'string', str: string } & WithX + + function getValueAsString_inline_if(value: BoxInline): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + } + + value; // { kind: "string", str: string } & { x: string } + return value.str; + } + + function getValueAsString_inline_switch(value: BoxInline): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + + case 'string': + value; // { kind: "string", str: string } & { x: string } + return value.str; + } + } + + function getValueAsString_if(value: BoxIntersection): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + } + + value; // { kind: "string", str: string } & { x: string } + return value.str; + } + + function getValueAsString_switch(value: BoxIntersection): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + + case 'string': + value; // { kind: "string", str: string } & { x: string } + return value.str; + } + } + + function getValueAsString_if_fixed(value: BoxIntersection & { kind: 'int' }): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } & { kind: "number" } + return '' + value.num; + } + + value; // never + return value.str; + ~~~ +!!! error TS2339: Property 'str' does not exist on type 'never'. + } + + function getValueAsString_switch_fixed(value: BoxIntersection & { kind: 'int' }): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } & { kind: "number" } + return '' + value.num; + + case 'string': + value; // never + return value.str; + ~~~ +!!! error TS2339: Property 'str' does not exist on type 'never'. + } + } + + function getValueAsString_if_never(value: BoxIntersection & { kind: number }): string { + if (value.kind === 'int') { + value; // never + return '' + value.num; + ~~~ +!!! error TS2339: Property 'num' does not exist on type 'never'. + } + + value; // { kind: "string", str: string } & { x: string } & { kind: "number" } + return value.str; + } + + function getValueAsString_switch_never(value: BoxIntersection & { kind: number }): string { + switch (value.kind) { + case 'int': + value; // never + return '' + value.num; + ~~~ +!!! error TS2339: Property 'num' does not exist on type 'never'. + + case 'string': + value; // never + return value.str; + ~~~ +!!! error TS2339: Property 'str' does not exist on type 'never'. + } + } + + type Ext = { strVal: string } & { kind: 'int' } + | { intVal: string } & { kind: 'string' } + + type Boxed2 = { kind: 'null' } + | BoxIntersection & Ext; + + function arbitraryNesting_if(value: Boxed2): void { + if (value.kind === 'null') { + return; + } + + if (value.kind === 'int') { + value; // { kind: 'int', num: number } & { x: string; } + value.x; + value.num; + value.strVal; + return; + } + + value; // { kind: 'string', str: string } & { x: string } & { intVal: string } & { kind: 'string' } + value.x; + value.str; + value.intVal; + } + + function arbitraryNesting_switch(value: Boxed2): void { + switch (value.kind) { + case 'null': + return; + + case 'int': + value.x; + value.num; + value.strVal; + return; + + case 'string': + value.x; + value.str; + value.intVal; + } + } + \ No newline at end of file diff --git a/tests/baselines/reference/discriminatedUnionTypesWithIntersection.js b/tests/baselines/reference/discriminatedUnionTypesWithIntersection.js new file mode 100644 index 0000000000000..2ce38241745d3 --- /dev/null +++ b/tests/baselines/reference/discriminatedUnionTypesWithIntersection.js @@ -0,0 +1,249 @@ +//// [discriminatedUnionTypesWithIntersection.ts] +type WithX = { + x: string; +} + +type BoxedValue = { kind: 'int', num: number } + | { kind: 'string', str: string } + +type BoxIntersection = BoxedValue & WithX + +type BoxInline = { kind: 'int', num: number } & WithX + | { kind: 'string', str: string } & WithX + +function getValueAsString_inline_if(value: BoxInline): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + } + + value; // { kind: "string", str: string } & { x: string } + return value.str; +} + +function getValueAsString_inline_switch(value: BoxInline): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + + case 'string': + value; // { kind: "string", str: string } & { x: string } + return value.str; + } +} + +function getValueAsString_if(value: BoxIntersection): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + } + + value; // { kind: "string", str: string } & { x: string } + return value.str; +} + +function getValueAsString_switch(value: BoxIntersection): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + + case 'string': + value; // { kind: "string", str: string } & { x: string } + return value.str; + } +} + +function getValueAsString_if_fixed(value: BoxIntersection & { kind: 'int' }): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } & { kind: "number" } + return '' + value.num; + } + + value; // never + return value.str; +} + +function getValueAsString_switch_fixed(value: BoxIntersection & { kind: 'int' }): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } & { kind: "number" } + return '' + value.num; + + case 'string': + value; // never + return value.str; + } +} + +function getValueAsString_if_never(value: BoxIntersection & { kind: number }): string { + if (value.kind === 'int') { + value; // never + return '' + value.num; + } + + value; // { kind: "string", str: string } & { x: string } & { kind: "number" } + return value.str; +} + +function getValueAsString_switch_never(value: BoxIntersection & { kind: number }): string { + switch (value.kind) { + case 'int': + value; // never + return '' + value.num; + + case 'string': + value; // never + return value.str; + } +} + +type Ext = { strVal: string } & { kind: 'int' } + | { intVal: string } & { kind: 'string' } + +type Boxed2 = { kind: 'null' } + | BoxIntersection & Ext; + +function arbitraryNesting_if(value: Boxed2): void { + if (value.kind === 'null') { + return; + } + + if (value.kind === 'int') { + value; // { kind: 'int', num: number } & { x: string; } + value.x; + value.num; + value.strVal; + return; + } + + value; // { kind: 'string', str: string } & { x: string } & { intVal: string } & { kind: 'string' } + value.x; + value.str; + value.intVal; +} + +function arbitraryNesting_switch(value: Boxed2): void { + switch (value.kind) { + case 'null': + return; + + case 'int': + value.x; + value.num; + value.strVal; + return; + + case 'string': + value.x; + value.str; + value.intVal; + } +} + + +//// [discriminatedUnionTypesWithIntersection.js] +function getValueAsString_inline_if(value) { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + } + value; // { kind: "string", str: string } & { x: string } + return value.str; +} +function getValueAsString_inline_switch(value) { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + case 'string': + value; // { kind: "string", str: string } & { x: string } + return value.str; + } +} +function getValueAsString_if(value) { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + } + value; // { kind: "string", str: string } & { x: string } + return value.str; +} +function getValueAsString_switch(value) { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + case 'string': + value; // { kind: "string", str: string } & { x: string } + return value.str; + } +} +function getValueAsString_if_fixed(value) { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } & { kind: "number" } + return '' + value.num; + } + value; // never + return value.str; +} +function getValueAsString_switch_fixed(value) { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } & { kind: "number" } + return '' + value.num; + case 'string': + value; // never + return value.str; + } +} +function getValueAsString_if_never(value) { + if (value.kind === 'int') { + value; // never + return '' + value.num; + } + value; // { kind: "string", str: string } & { x: string } & { kind: "number" } + return value.str; +} +function getValueAsString_switch_never(value) { + switch (value.kind) { + case 'int': + value; // never + return '' + value.num; + case 'string': + value; // never + return value.str; + } +} +function arbitraryNesting_if(value) { + if (value.kind === 'null') { + return; + } + if (value.kind === 'int') { + value; // { kind: 'int', num: number } & { x: string; } + value.x; + value.num; + value.strVal; + return; + } + value; // { kind: 'string', str: string } & { x: string } & { intVal: string } & { kind: 'string' } + value.x; + value.str; + value.intVal; +} +function arbitraryNesting_switch(value) { + switch (value.kind) { + case 'null': + return; + case 'int': + value.x; + value.num; + value.strVal; + return; + case 'string': + value.x; + value.str; + value.intVal; + } +} diff --git a/tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts b/tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts new file mode 100644 index 0000000000000..63d3e001d8321 --- /dev/null +++ b/tests/cases/conformance/types/union/discriminatedUnionTypesWithIntersection.ts @@ -0,0 +1,142 @@ +type WithX = { + x: string; +} + +type BoxedValue = { kind: 'int', num: number } + | { kind: 'string', str: string } + +type BoxIntersection = BoxedValue & WithX + +type BoxInline = { kind: 'int', num: number } & WithX + | { kind: 'string', str: string } & WithX + +function getValueAsString_inline_if(value: BoxInline): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + } + + value; // { kind: "string", str: string } & { x: string } + return value.str; +} + +function getValueAsString_inline_switch(value: BoxInline): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + + case 'string': + value; // { kind: "string", str: string } & { x: string } + return value.str; + } +} + +function getValueAsString_if(value: BoxIntersection): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + } + + value; // { kind: "string", str: string } & { x: string } + return value.str; +} + +function getValueAsString_switch(value: BoxIntersection): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } + return '' + value.num; + + case 'string': + value; // { kind: "string", str: string } & { x: string } + return value.str; + } +} + +function getValueAsString_if_fixed(value: BoxIntersection & { kind: 'int' }): string { + if (value.kind === 'int') { + value; // { kind: "int", num: number } & { x: string } & { kind: "number" } + return '' + value.num; + } + + value; // never + return value.str; +} + +function getValueAsString_switch_fixed(value: BoxIntersection & { kind: 'int' }): string { + switch (value.kind) { + case 'int': + value; // { kind: "int", num: number } & { x: string } & { kind: "number" } + return '' + value.num; + + case 'string': + value; // never + return value.str; + } +} + +function getValueAsString_if_never(value: BoxIntersection & { kind: number }): string { + if (value.kind === 'int') { + value; // never + return '' + value.num; + } + + value; // { kind: "string", str: string } & { x: string } & { kind: "number" } + return value.str; +} + +function getValueAsString_switch_never(value: BoxIntersection & { kind: number }): string { + switch (value.kind) { + case 'int': + value; // never + return '' + value.num; + + case 'string': + value; // never + return value.str; + } +} + +type Ext = { strVal: string } & { kind: 'int' } + | { intVal: string } & { kind: 'string' } + +type Boxed2 = { kind: 'null' } + | BoxIntersection & Ext; + +function arbitraryNesting_if(value: Boxed2): void { + if (value.kind === 'null') { + return; + } + + if (value.kind === 'int') { + value; // { kind: 'int', num: number } & { x: string; } + value.x; + value.num; + value.strVal; + return; + } + + value; // { kind: 'string', str: string } & { x: string } & { intVal: string } & { kind: 'string' } + value.x; + value.str; + value.intVal; +} + +function arbitraryNesting_switch(value: Boxed2): void { + switch (value.kind) { + case 'null': + return; + + case 'int': + value.x; + value.num; + value.strVal; + return; + + case 'string': + value.x; + value.str; + value.intVal; + } +}