diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 2a4ead5003a89..61812da7fbf8b 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -18533,6 +18533,12 @@ func (c *Checker) getWidenedTypeOfObjectLiteral(t *Type, context *WideningContex })) // Retain js literal flag through widening result.objectFlags |= t.objectFlags & (ObjectFlagsJSLiteral | ObjectFlagsNonInferrableType) + // Remember that this type originates in an object literal so that later subtype checks (e.g. when + // computing a common supertype) don't treat it as though it were a "regular" (non-literal) type that + // requires target's optional properties to be present (see 'requireOptionalProperties' in relater.go). + if isObjectLiteralType(t) { + result.objectFlags |= ObjectFlagsWidenedObjectLiteral + } // Only cache in child contexts since the root context never widens a particular object literal type more than once if context != nil && context.parent != nil { if context.widenedTypes == nil { diff --git a/tsc/internal/checker/relater.go b/tsc/internal/checker/relater.go index 9008420fb7cd1..04eca213bfb41 100644 --- a/tsc/internal/checker/relater.go +++ b/tsc/internal/checker/relater.go @@ -4261,7 +4261,7 @@ func (r *Relater) propertiesRelatedTo(source *Type, target *Type, reportErrors b return TernaryFalse } } - requireOptionalProperties := (r.relation == r.c.subtypeRelation || r.relation == r.c.strictSubtypeRelation) && !isObjectLiteralType(source) && !r.c.isEmptyArrayLiteralType(source) && !isTupleType(source) + requireOptionalProperties := (r.relation == r.c.subtypeRelation || r.relation == r.c.strictSubtypeRelation) && !isObjectLiteralType(source) && source.objectFlags&ObjectFlagsWidenedObjectLiteral == 0 && !r.c.isEmptyArrayLiteralType(source) && !isTupleType(source) unmatchedProperty := r.c.getUnmatchedProperty(source, target, requireOptionalProperties, false /*matchDiscriminantProperties*/) if unmatchedProperty != nil { if reportErrors && r.c.shouldReportUnmatchedPropertyError(source, target) { diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index 2e8bacbd5df29..177e9cee24d06 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -619,6 +619,7 @@ const ( ObjectFlagsCouldContainTypeVariablesComputed ObjectFlags = 1 << 19 // CouldContainTypeVariables flag has been computed ObjectFlagsCouldContainTypeVariables ObjectFlags = 1 << 20 // Type could contain a type variable ObjectFlagsMembersResolved ObjectFlags = 1 << 21 // Members have been resolved + ObjectFlagsWidenedObjectLiteral ObjectFlags = 1 << 31 // Originates in an object literal type that has since been widened ObjectFlagsClassOrInterface = ObjectFlagsClass | ObjectFlagsInterface ObjectFlagsRequiresWidening = ObjectFlagsContainsWideningType | ObjectFlagsContainsObjectOrArrayLiteral diff --git a/tsc/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.symbols b/tsc/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.symbols new file mode 100644 index 0000000000000..33deb278c9a5c --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.symbols @@ -0,0 +1,48 @@ +//// [tests/cases/compiler/discriminatedUnionFlatMap.ts] //// + +=== discriminatedUnionFlatMap.ts === +// https://github.com/microsoft/TypeScript/issues/63776 + +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/tsc/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.types b/tsc/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.types new file mode 100644 index 0000000000000..6794e2294b43f --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/discriminatedUnionFlatMap.types @@ -0,0 +1,57 @@ +//// [tests/cases/compiler/discriminatedUnionFlatMap.ts] //// + +=== discriminatedUnionFlatMap.ts === +// https://github.com/microsoft/TypeScript/issues/63776 + +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[] +>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/tsc/testdata/tests/cases/compiler/discriminatedUnionFlatMap.ts b/tsc/testdata/tests/cases/compiler/discriminatedUnionFlatMap.ts new file mode 100644 index 0000000000000..7ff8f6792df52 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/discriminatedUnionFlatMap.ts @@ -0,0 +1,18 @@ +// @strict: true +// @noEmit: true +// @lib: es2019 + +// https://github.com/microsoft/TypeScript/issues/63776 + +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]; + } + }); +}