diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 0567712f11da3..84af60db00c18 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -25499,12 +25499,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function getSingleCommonSupertype(types: 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). const candidate = reduceLeft(types, (s, t) => isTypeStrictSubtypeOf(s, t) ? t : s)!; return every(types, t => t === candidate || isTypeStrictSubtypeOf(t, candidate)) ? candidate : - reduceLeft(types, (s, t) => isTypeSubtypeOf(s, t) ? t : s)!; + reduceLeft(types, (s, t) => isTypeAssignableTo(s, t) && !isTypeAssignableTo(t, s) ? t : s)!; } // Return the leftmost type for which no type to the right is a subtype. diff --git a/tests/baselines/reference/discriminatedUnionFlatMap.symbols b/tests/baselines/reference/discriminatedUnionFlatMap.symbols new file mode 100644 index 0000000000000..0bcab917b79c9 --- /dev/null +++ b/tests/baselines/reference/discriminatedUnionFlatMap.symbols @@ -0,0 +1,48 @@ +//// [tests/cases/compiler/discriminatedUnionFlatMap.ts] //// + +=== discriminatedUnionFlatMap.ts === +// https://github.com/microsoft/typescript-go/issues/1057 + +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, 31)) + + 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, 31)) +>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, 31)) +>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, 34)) + + } else { + return [operation]; +>operation : Symbol(operation, Decl(discriminatedUnionFlatMap.ts, 6, 31)) + } + }); +} + diff --git a/tests/baselines/reference/discriminatedUnionFlatMap.types b/tests/baselines/reference/discriminatedUnionFlatMap.types new file mode 100644 index 0000000000000..6b318796592b7 --- /dev/null +++ b/tests/baselines/reference/discriminatedUnionFlatMap.types @@ -0,0 +1,91 @@ +//// [tests/cases/compiler/discriminatedUnionFlatMap.ts] //// + +=== discriminatedUnionFlatMap.ts === +// https://github.com/microsoft/typescript-go/issues/1057 + +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) => InputOp[] | { op: "remove"; }[] +> : ^ ^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>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/tests/cases/compiler/discriminatedUnionFlatMap.ts b/tests/cases/compiler/discriminatedUnionFlatMap.ts new file mode 100644 index 0000000000000..688e18b4e5313 --- /dev/null +++ b/tests/cases/compiler/discriminatedUnionFlatMap.ts @@ -0,0 +1,18 @@ +// @strict: true +// @noEmit: true +// @lib: es2019 + +// https://github.com/microsoft/typescript-go/issues/1057 + +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]; + } + }); +}