diff --git a/internal/checker/inference.go b/internal/checker/inference.go index 64ea6d8ce70..bc4fccdf3e3 100644 --- a/internal/checker/inference.go +++ b/internal/checker/inference.go @@ -1482,13 +1482,15 @@ func (c *Checker) getCommonSupertype(types []*Type) *Type { func (c *Checker) getSingleCommonSupertype(types []*Type) *Type { // First, find the leftmost type for which no type to the right is a strict supertype, and if that - // type is a strict supertype of all other candidates, return it. Otherwise, return the leftmost type - // for which no type to the right is a (regular) supertype. + // type is a strict supertype of all other candidates, return it. Otherwise, use asymmetric + // assignability as a tiebreaker (i.e. only switch candidates when the assignment is one-way). candidate := c.findLeftmostType(types, (*Checker).isTypeStrictSubtypeOf) if core.Every(types, func(t *Type) bool { return t == candidate || c.isTypeStrictSubtypeOf(t, candidate) }) { return candidate } - return c.findLeftmostType(types, (*Checker).isTypeSubtypeOf) + return c.findLeftmostType(types, func(c *Checker, s *Type, t *Type) bool { + return c.isTypeAssignableTo(s, t) && !c.isTypeAssignableTo(t, s) + }) } func (c *Checker) findLeftmostType(types []*Type, f func(c *Checker, s *Type, t *Type) bool) *Type { diff --git a/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.symbols b/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.symbols new file mode 100644 index 00000000000..88b3c65f688 --- /dev/null +++ b/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.symbols @@ -0,0 +1,48 @@ +//// [tests/cases/compiler/discriminatedUnionFlatMap.ts] //// + +=== discriminatedUnionFlatMap.ts === +// https://github.com/microsoft/typescript-go/issues/2149 + +export type InputOp = { op: "add" } | { op: "remove"; value?: Array }; +>InputOp : Symbol(InputOp, Decl(discriminatedUnionFlatMap.ts, 0, 0)) +>op : Symbol(op, Decl(discriminatedUnionFlatMap.ts, 2, 23)) +>op : Symbol(op, Decl(discriminatedUnionFlatMap.ts, 2, 39)) +>value : Symbol(value, Decl(discriminatedUnionFlatMap.ts, 2, 53)) +>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 2 more) + +export type OutputOp = { op: "add" | "remove" }; +>OutputOp : Symbol(OutputOp, Decl(discriminatedUnionFlatMap.ts, 2, 79)) +>op : Symbol(op, Decl(discriminatedUnionFlatMap.ts, 3, 24)) + +export function f(operations: InputOp[]): OutputOp[] { +>f : Symbol(f, Decl(discriminatedUnionFlatMap.ts, 3, 48)) +>operations : Symbol(operations, Decl(discriminatedUnionFlatMap.ts, 5, 18)) +>InputOp : Symbol(InputOp, Decl(discriminatedUnionFlatMap.ts, 0, 0)) +>OutputOp : Symbol(OutputOp, Decl(discriminatedUnionFlatMap.ts, 2, 79)) + + return operations.flatMap((operation) => { +>operations.flatMap : Symbol(Array.flatMap, Decl(lib.es2019.array.d.ts, --, --)) +>operations : Symbol(operations, Decl(discriminatedUnionFlatMap.ts, 5, 18)) +>flatMap : Symbol(Array.flatMap, Decl(lib.es2019.array.d.ts, --, --)) +>operation : Symbol(operation, Decl(discriminatedUnionFlatMap.ts, 6, 29)) + + if (operation.op === "remove" && operation.value) { +>operation.op : Symbol(op, Decl(discriminatedUnionFlatMap.ts, 2, 23), Decl(discriminatedUnionFlatMap.ts, 2, 39)) +>operation : Symbol(operation, Decl(discriminatedUnionFlatMap.ts, 6, 29)) +>op : Symbol(op, Decl(discriminatedUnionFlatMap.ts, 2, 23), Decl(discriminatedUnionFlatMap.ts, 2, 39)) +>operation.value : Symbol(value, Decl(discriminatedUnionFlatMap.ts, 2, 53)) +>operation : Symbol(operation, Decl(discriminatedUnionFlatMap.ts, 6, 29)) +>value : Symbol(value, Decl(discriminatedUnionFlatMap.ts, 2, 53)) + + return [].map(() => ({ op: "remove" })); +>[].map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>map : Symbol(Array.map, Decl(lib.es5.d.ts, --, --)) +>op : Symbol(op, Decl(discriminatedUnionFlatMap.ts, 8, 28)) + + } else { + return [operation]; +>operation : Symbol(operation, Decl(discriminatedUnionFlatMap.ts, 6, 29)) + } + }); +} + diff --git a/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.types b/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.types new file mode 100644 index 00000000000..724d0600435 --- /dev/null +++ b/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.types @@ -0,0 +1,57 @@ +//// [tests/cases/compiler/discriminatedUnionFlatMap.ts] //// + +=== discriminatedUnionFlatMap.ts === +// https://github.com/microsoft/typescript-go/issues/2149 + +export type InputOp = { op: "add" } | { op: "remove"; value?: Array }; +>InputOp : InputOp +>op : "add" +>op : "remove" +>value : unknown[] | undefined + +export type OutputOp = { op: "add" | "remove" }; +>OutputOp : OutputOp +>op : "add" | "remove" + +export function f(operations: InputOp[]): OutputOp[] { +>f : (operations: InputOp[]) => OutputOp[] +>operations : InputOp[] + + return operations.flatMap((operation) => { +>operations.flatMap((operation) => { if (operation.op === "remove" && operation.value) { return [].map(() => ({ op: "remove" })); } else { return [operation]; } }) : InputOp[] +>operations.flatMap : (callback: (this: This, value: InputOp, index: number, array: InputOp[]) => U | readonly U[], thisArg?: This | undefined) => U[] +>operations : InputOp[] +>flatMap : (callback: (this: This, value: InputOp, index: number, array: InputOp[]) => U | readonly U[], thisArg?: This | undefined) => U[] +>(operation) => { if (operation.op === "remove" && operation.value) { return [].map(() => ({ op: "remove" })); } else { return [operation]; } } : (this: undefined, operation: InputOp) => { op: "remove"; }[] | InputOp[] +>operation : InputOp + + if (operation.op === "remove" && operation.value) { +>operation.op === "remove" && operation.value : false | unknown[] | undefined +>operation.op === "remove" : boolean +>operation.op : "add" | "remove" +>operation : InputOp +>op : "add" | "remove" +>"remove" : "remove" +>operation.value : unknown[] | undefined +>operation : { op: "remove"; value?: Array; } +>value : unknown[] | undefined + + return [].map(() => ({ op: "remove" })); +>[].map(() => ({ op: "remove" })) : { op: "remove"; }[] +>[].map : (callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[] +>[] : never[] +>map : (callbackfn: (value: never, index: number, array: never[]) => U, thisArg?: any) => U[] +>() => ({ op: "remove" }) : () => { op: "remove"; } +>({ op: "remove" }) : { op: "remove"; } +>{ op: "remove" } : { op: "remove"; } +>op : "remove" +>"remove" : "remove" + + } else { + return [operation]; +>[operation] : InputOp[] +>operation : InputOp + } + }); +} + diff --git a/testdata/tests/cases/compiler/discriminatedUnionFlatMap.ts b/testdata/tests/cases/compiler/discriminatedUnionFlatMap.ts new file mode 100644 index 00000000000..cacb1718ceb --- /dev/null +++ b/testdata/tests/cases/compiler/discriminatedUnionFlatMap.ts @@ -0,0 +1,18 @@ +// @strict: true +// @noEmit: true +// @lib: es2019 + +// https://github.com/microsoft/typescript-go/issues/2149 + +export type InputOp = { op: "add" } | { op: "remove"; value?: Array }; +export type OutputOp = { op: "add" | "remove" }; + +export function f(operations: InputOp[]): OutputOp[] { + return operations.flatMap((operation) => { + if (operation.op === "remove" && operation.value) { + return [].map(() => ({ op: "remove" })); + } else { + return [operation]; + } + }); +}