From c2a40044744ebca59e5fb5088901bb8b89df7143 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 24 Oct 2026 17:28:36 +0200 Subject: [PATCH 1/5] Skip the narrowable-reference walk of unions known to contain no type variables --- tsc/internal/checker/checker.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 4c57dd37910f2..8fea787c1ad5e 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -32269,7 +32269,7 @@ func (c *Checker) getNarrowableTypeForReference(t *Type, reference *ast.Node, ch // control flow analysis an opportunity to narrow it further. For example, for a reference of a type // parameter type 'T extends string | undefined' with a contextual type 'string', we substitute // 'string | undefined' to give control flow analysis the opportunity to narrow to type 'string'. - substituteConstraints := checkMode&CheckModeInferential == 0 && someType(t, c.isGenericTypeWithUnionConstraint) && (c.isConstraintPosition(t, reference) || c.hasContextualTypeWithNoGenericTypes(reference, checkMode)) + substituteConstraints := checkMode&CheckModeInferential == 0 && c.someGenericTypeWithUnionConstraint(t) && (c.isConstraintPosition(t, reference) || c.hasContextualTypeWithNoGenericTypes(reference, checkMode)) if substituteConstraints { return c.mapType(t, c.getBaseConstraintOrType) } @@ -32286,6 +32286,17 @@ func (c *Checker) isConstraintPosition(t *Type, node *ast.Node) bool { ast.IsElementAccessExpression(parent) && parent.Expression() == node && !(someType(t, c.isGenericTypeWithoutNullableConstraint) && c.isGenericIndexType(c.getTypeOfExpression(parent.AsElementAccessExpression().ArgumentExpression))) } +// A union whose couldContainTypeVariables result is already computed as false has no constituent that is +// a generic type with a union constraint, so the per-constituent walk can be skipped. The only instantiable +// constituents such a union can hold are pattern literal types of a non-generic top-level alias, and each of +// those is its own base constraint. The cached result is only read here, never computed. +func (c *Checker) someGenericTypeWithUnionConstraint(t *Type) bool { + if t.flags&TypeFlagsUnion != 0 && t.objectFlags&(ObjectFlagsCouldContainTypeVariablesComputed|ObjectFlagsCouldContainTypeVariables) == ObjectFlagsCouldContainTypeVariablesComputed { + return false + } + return someType(t, c.isGenericTypeWithUnionConstraint) +} + func (c *Checker) isGenericTypeWithUnionConstraint(t *Type) bool { if t.flags&TypeFlagsIntersection != 0 { return core.Some(t.AsIntersectionType().types, c.isGenericTypeWithUnionConstraint) From 3d0bb919b5a992dfe05ae2e07406b03890cd6047 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sun, 25 Oct 2026 00:44:47 +0200 Subject: [PATCH 2/5] Compute couldContainTypeVariables when skipping the narrowable-reference union walk --- tsc/internal/checker/checker.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 8fea787c1ad5e..8507c8833a087 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -32286,12 +32286,15 @@ func (c *Checker) isConstraintPosition(t *Type, node *ast.Node) bool { ast.IsElementAccessExpression(parent) && parent.Expression() == node && !(someType(t, c.isGenericTypeWithoutNullableConstraint) && c.isGenericIndexType(c.getTypeOfExpression(parent.AsElementAccessExpression().ArgumentExpression))) } -// A union whose couldContainTypeVariables result is already computed as false has no constituent that is -// a generic type with a union constraint, so the per-constituent walk can be skipped. The only instantiable -// constituents such a union can hold are pattern literal types of a non-generic top-level alias, and each of -// those is its own base constraint. The cached result is only read here, never computed. +// Skips the walk for unions that cannot contain type variables. Such a union can still hold +// instantiable types when it comes from a non-generic top-level alias: +// +// type Size = `${number}px` | Uppercase | "auto" +// +// Both `${number}px` and Uppercase are their own base constraints, never a union, so the +// walk would return false anyway. func (c *Checker) someGenericTypeWithUnionConstraint(t *Type) bool { - if t.flags&TypeFlagsUnion != 0 && t.objectFlags&(ObjectFlagsCouldContainTypeVariablesComputed|ObjectFlagsCouldContainTypeVariables) == ObjectFlagsCouldContainTypeVariablesComputed { + if t.flags&TypeFlagsUnion != 0 && !c.couldContainTypeVariables(t) { return false } return someType(t, c.isGenericTypeWithUnionConstraint) From 222434d6eeebc3d3a8a10e37e3dbe927402791d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 26 Oct 2026 11:02:01 +0100 Subject: [PATCH 3/5] Explain the eager couldContainTypeVariables cost and the union-only check --- tsc/internal/checker/checker.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 8507c8833a087..2eb0f5cc29b1b 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -32289,10 +32289,17 @@ func (c *Checker) isConstraintPosition(t *Type, node *ast.Node) bool { // Skips the walk for unions that cannot contain type variables. Such a union can still hold // instantiable types when it comes from a non-generic top-level alias: // -// type Size = `${number}px` | Uppercase | "auto" +// type Size = `${number}px` | Uppercase | "auto" // // Both `${number}px` and Uppercase are their own base constraints, never a union, so the // walk would return false anyway. +// +// In theory couldContainTypeVariables can do more work than the walk, as it also looks into the type +// arguments of object types. But its result is cached on the type and it usually gets computed elsewhere +// anyway, so this one-time eager cost pays off on later references to the same union. +// +// Only unions are checked, because for any other type the walk is a single call that is already as +// cheap as the check that would skip it. func (c *Checker) someGenericTypeWithUnionConstraint(t *Type) bool { if t.flags&TypeFlagsUnion != 0 && !c.couldContainTypeVariables(t) { return false From d82bdcbcfefceb24bfdf47b741ff8ccfffa48c3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 26 Oct 2026 11:58:55 +0100 Subject: [PATCH 4/5] Skip the narrowable-reference walk only for unions without instantiable or intersection constituents --- tsc/internal/checker/checker.go | 24 ++---- tsc/internal/checker/types.go | 1 + .../narrowingNoInferUnionConstraint.symbols | 82 +++++++++++++++++++ .../narrowingNoInferUnionConstraint.types | 76 +++++++++++++++++ .../narrowingNoInferUnionConstraint.ts | 31 +++++++ 5 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 tsc/testdata/baselines/reference/compiler/narrowingNoInferUnionConstraint.symbols create mode 100644 tsc/testdata/baselines/reference/compiler/narrowingNoInferUnionConstraint.types create mode 100644 tsc/testdata/tests/cases/compiler/narrowingNoInferUnionConstraint.ts diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 2eb0f5cc29b1b..0d74f0595e883 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -26470,7 +26470,11 @@ func (c *Checker) getUnionTypeFromSortedList(types []*Type, precomputedObjectFla key := getUnionKey(types, origin, alias) t := c.unionTypes[key] if t == nil { - t = c.newUnionType(precomputedObjectFlags|c.getPropagatingFlagsOfTypes(types, TypeFlagsNullable), types) + objectFlags := precomputedObjectFlags | c.getPropagatingFlagsOfTypes(types, TypeFlagsNullable) + if core.Some(types, func(t *Type) bool { return t.flags&TypeFlagsInstantiable != 0 }) { + objectFlags |= ObjectFlagsContainsInstantiables + } + t = c.newUnionType(objectFlags, types) t.AsUnionType().origin = origin t.alias = alias if len(types) == 2 && types[0].flags&TypeFlagsBooleanLiteral != 0 && types[1].flags&TypeFlagsBooleanLiteral != 0 { @@ -32286,22 +32290,10 @@ func (c *Checker) isConstraintPosition(t *Type, node *ast.Node) bool { ast.IsElementAccessExpression(parent) && parent.Expression() == node && !(someType(t, c.isGenericTypeWithoutNullableConstraint) && c.isGenericIndexType(c.getTypeOfExpression(parent.AsElementAccessExpression().ArgumentExpression))) } -// Skips the walk for unions that cannot contain type variables. Such a union can still hold -// instantiable types when it comes from a non-generic top-level alias: -// -// type Size = `${number}px` | Uppercase | "auto" -// -// Both `${number}px` and Uppercase are their own base constraints, never a union, so the -// walk would return false anyway. -// -// In theory couldContainTypeVariables can do more work than the walk, as it also looks into the type -// arguments of object types. But its result is cached on the type and it usually gets computed elsewhere -// anyway, so this one-time eager cost pays off on later references to the same union. -// -// Only unions are checked, because for any other type the walk is a single call that is already as -// cheap as the check that would skip it. +// A union type is a generic type with a union constraint only when one of its constituents is instantiable, or is +// an intersection that might contain one, so the walk is skipped for unions with neither. func (c *Checker) someGenericTypeWithUnionConstraint(t *Type) bool { - if t.flags&TypeFlagsUnion != 0 && !c.couldContainTypeVariables(t) { + if t.flags&TypeFlagsUnion != 0 && t.objectFlags&(ObjectFlagsContainsInstantiables|ObjectFlagsContainsIntersections) == 0 { return false } return someType(t, c.isGenericTypeWithUnionConstraint) diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index f119288a21744..cfdef60b0f684 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -726,6 +726,7 @@ const ( ObjectFlagsIsUnknownLikeUnion = 1 << 27 // Union of null, undefined, and empty object type ObjectFlagsIsUniformEnumComputed = 1 << 28 // IsUniformEnum flag has been computed ObjectFlagsIsUniformEnum = 1 << 29 // Union contains uniform literal types + ObjectFlagsContainsInstantiables = 1 << 30 // Union contains instantiable types // Flags that require TypeFlags.Intersection ObjectFlagsIsNeverIntersectionComputed = 1 << 25 // IsNeverLike flag has been computed ObjectFlagsIsNeverIntersection = 1 << 26 // Intersection reduces to never diff --git a/tsc/testdata/baselines/reference/compiler/narrowingNoInferUnionConstraint.symbols b/tsc/testdata/baselines/reference/compiler/narrowingNoInferUnionConstraint.symbols new file mode 100644 index 0000000000000..a1297cb2fbe00 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/narrowingNoInferUnionConstraint.symbols @@ -0,0 +1,82 @@ +//// [tests/cases/compiler/narrowingNoInferUnionConstraint.ts] //// + +=== narrowingNoInferUnionConstraint.ts === +// A non-generic top-level type alias can hold a NoInfer type whose constraint is a union. A reference to it is +// narrowed through that constraint. + +type U1 = NoInfer<{ kind: "a"; a: number } | { kind: "b"; b: number }> | { kind: "c" }; +>U1 : Symbol(U1, Decl(narrowingNoInferUnionConstraint.ts, 0, 0)) +>NoInfer : Symbol(NoInfer, Decl(lib.es5.d.ts, --, --)) +>kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 3, 19)) +>a : Symbol(a, Decl(narrowingNoInferUnionConstraint.ts, 3, 30)) +>kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 3, 46)) +>b : Symbol(b, Decl(narrowingNoInferUnionConstraint.ts, 3, 57)) +>kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 3, 74)) + +function f1(u: U1) { +>f1 : Symbol(f1, Decl(narrowingNoInferUnionConstraint.ts, 3, 87)) +>u : Symbol(u, Decl(narrowingNoInferUnionConstraint.ts, 5, 12)) +>U1 : Symbol(U1, Decl(narrowingNoInferUnionConstraint.ts, 0, 0)) + + if (u.kind === "a") { +>u.kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 3, 19), Decl(narrowingNoInferUnionConstraint.ts, 3, 46), Decl(narrowingNoInferUnionConstraint.ts, 3, 74)) +>u : Symbol(u, Decl(narrowingNoInferUnionConstraint.ts, 5, 12)) +>kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 3, 19), Decl(narrowingNoInferUnionConstraint.ts, 3, 46), Decl(narrowingNoInferUnionConstraint.ts, 3, 74)) + + u.a; +>u.a : Symbol(a, Decl(narrowingNoInferUnionConstraint.ts, 3, 30)) +>u : Symbol(u, Decl(narrowingNoInferUnionConstraint.ts, 5, 12)) +>a : Symbol(a, Decl(narrowingNoInferUnionConstraint.ts, 3, 30)) + } +} + +// The same after an instantiation of Box has computed whether U2 could contain type variables. + +type U2 = NoInfer<{ kind: "a"; a: number } | { kind: "b"; b: number }> | { kind: "c" }; +>U2 : Symbol(U2, Decl(narrowingNoInferUnionConstraint.ts, 9, 1)) +>NoInfer : Symbol(NoInfer, Decl(lib.es5.d.ts, --, --)) +>kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 13, 19)) +>a : Symbol(a, Decl(narrowingNoInferUnionConstraint.ts, 13, 30)) +>kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 13, 46)) +>b : Symbol(b, Decl(narrowingNoInferUnionConstraint.ts, 13, 57)) +>kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 13, 74)) + +interface Box { +>Box : Symbol(Box, Decl(narrowingNoInferUnionConstraint.ts, 13, 87)) +>T : Symbol(T, Decl(narrowingNoInferUnionConstraint.ts, 15, 14)) + + u: U2; +>u : Symbol(Box.u, Decl(narrowingNoInferUnionConstraint.ts, 15, 18)) +>U2 : Symbol(U2, Decl(narrowingNoInferUnionConstraint.ts, 9, 1)) + + t: T; +>t : Symbol(Box.t, Decl(narrowingNoInferUnionConstraint.ts, 16, 10)) +>T : Symbol(T, Decl(narrowingNoInferUnionConstraint.ts, 15, 14)) +} + +declare const box: Box; +>box : Symbol(box, Decl(narrowingNoInferUnionConstraint.ts, 20, 13)) +>Box : Symbol(Box, Decl(narrowingNoInferUnionConstraint.ts, 13, 87)) + +box.u; +>box.u : Symbol(Box.u, Decl(narrowingNoInferUnionConstraint.ts, 15, 18)) +>box : Symbol(box, Decl(narrowingNoInferUnionConstraint.ts, 20, 13)) +>u : Symbol(Box.u, Decl(narrowingNoInferUnionConstraint.ts, 15, 18)) + +function f2(u: U2) { +>f2 : Symbol(f2, Decl(narrowingNoInferUnionConstraint.ts, 21, 6)) +>u : Symbol(u, Decl(narrowingNoInferUnionConstraint.ts, 23, 12)) +>U2 : Symbol(U2, Decl(narrowingNoInferUnionConstraint.ts, 9, 1)) + + if (u.kind === "a") { +>u.kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 13, 19), Decl(narrowingNoInferUnionConstraint.ts, 13, 46), Decl(narrowingNoInferUnionConstraint.ts, 13, 74)) +>u : Symbol(u, Decl(narrowingNoInferUnionConstraint.ts, 23, 12)) +>kind : Symbol(kind, Decl(narrowingNoInferUnionConstraint.ts, 13, 19), Decl(narrowingNoInferUnionConstraint.ts, 13, 46), Decl(narrowingNoInferUnionConstraint.ts, 13, 74)) + + u.a; +>u.a : Symbol(a, Decl(narrowingNoInferUnionConstraint.ts, 13, 30)) +>u : Symbol(u, Decl(narrowingNoInferUnionConstraint.ts, 23, 12)) +>a : Symbol(a, Decl(narrowingNoInferUnionConstraint.ts, 13, 30)) + } +} + diff --git a/tsc/testdata/baselines/reference/compiler/narrowingNoInferUnionConstraint.types b/tsc/testdata/baselines/reference/compiler/narrowingNoInferUnionConstraint.types new file mode 100644 index 0000000000000..de45d8a9479c1 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/narrowingNoInferUnionConstraint.types @@ -0,0 +1,76 @@ +//// [tests/cases/compiler/narrowingNoInferUnionConstraint.ts] //// + +=== narrowingNoInferUnionConstraint.ts === +// A non-generic top-level type alias can hold a NoInfer type whose constraint is a union. A reference to it is +// narrowed through that constraint. + +type U1 = NoInfer<{ kind: "a"; a: number } | { kind: "b"; b: number }> | { kind: "c" }; +>U1 : U1 +>kind : "a" +>a : number +>kind : "b" +>b : number +>kind : "c" + +function f1(u: U1) { +>f1 : (u: U1) => void +>u : U1 + + if (u.kind === "a") { +>u.kind === "a" : boolean +>u.kind : "a" | "b" | "c" +>u : { kind: "a"; a: number; } | { kind: "b"; b: number; } | { kind: "c"; } +>kind : "a" | "b" | "c" +>"a" : "a" + + u.a; +>u.a : number +>u : { kind: "a"; a: number; } +>a : number + } +} + +// The same after an instantiation of Box has computed whether U2 could contain type variables. + +type U2 = NoInfer<{ kind: "a"; a: number } | { kind: "b"; b: number }> | { kind: "c" }; +>U2 : U2 +>kind : "a" +>a : number +>kind : "b" +>b : number +>kind : "c" + +interface Box { + u: U2; +>u : U2 + + t: T; +>t : T +} + +declare const box: Box; +>box : Box + +box.u; +>box.u : U2 +>box : Box +>u : U2 + +function f2(u: U2) { +>f2 : (u: U2) => void +>u : U2 + + if (u.kind === "a") { +>u.kind === "a" : boolean +>u.kind : "a" | "b" | "c" +>u : { kind: "a"; a: number; } | { kind: "b"; b: number; } | { kind: "c"; } +>kind : "a" | "b" | "c" +>"a" : "a" + + u.a; +>u.a : number +>u : { kind: "a"; a: number; } +>a : number + } +} + diff --git a/tsc/testdata/tests/cases/compiler/narrowingNoInferUnionConstraint.ts b/tsc/testdata/tests/cases/compiler/narrowingNoInferUnionConstraint.ts new file mode 100644 index 0000000000000..d8439f90eaf1c --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/narrowingNoInferUnionConstraint.ts @@ -0,0 +1,31 @@ +// @strict: true +// @noEmit: true + +// A non-generic top-level type alias can hold a NoInfer type whose constraint is a union. A reference to it is +// narrowed through that constraint. + +type U1 = NoInfer<{ kind: "a"; a: number } | { kind: "b"; b: number }> | { kind: "c" }; + +function f1(u: U1) { + if (u.kind === "a") { + u.a; + } +} + +// The same after an instantiation of Box has computed whether U2 could contain type variables. + +type U2 = NoInfer<{ kind: "a"; a: number } | { kind: "b"; b: number }> | { kind: "c" }; + +interface Box { + u: U2; + t: T; +} + +declare const box: Box; +box.u; + +function f2(u: U2) { + if (u.kind === "a") { + u.a; + } +} From 768f7afea5b8e3dded8b4832b2b9a6971028cccb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Mon, 26 Oct 2026 12:26:21 +0100 Subject: [PATCH 5/5] Store whether a union has instantiable constituents on UnionType instead of in ObjectFlags --- tsc/internal/checker/checker.go | 9 +++------ tsc/internal/checker/types.go | 12 ++++++------ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 0d74f0595e883..e15d67d9f69bb 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -26470,11 +26470,8 @@ func (c *Checker) getUnionTypeFromSortedList(types []*Type, precomputedObjectFla key := getUnionKey(types, origin, alias) t := c.unionTypes[key] if t == nil { - objectFlags := precomputedObjectFlags | c.getPropagatingFlagsOfTypes(types, TypeFlagsNullable) - if core.Some(types, func(t *Type) bool { return t.flags&TypeFlagsInstantiable != 0 }) { - objectFlags |= ObjectFlagsContainsInstantiables - } - t = c.newUnionType(objectFlags, types) + t = c.newUnionType(precomputedObjectFlags|c.getPropagatingFlagsOfTypes(types, TypeFlagsNullable), types) + t.AsUnionType().containsInstantiables = core.Some(types, func(t *Type) bool { return t.flags&TypeFlagsInstantiable != 0 }) t.AsUnionType().origin = origin t.alias = alias if len(types) == 2 && types[0].flags&TypeFlagsBooleanLiteral != 0 && types[1].flags&TypeFlagsBooleanLiteral != 0 { @@ -32293,7 +32290,7 @@ func (c *Checker) isConstraintPosition(t *Type, node *ast.Node) bool { // A union type is a generic type with a union constraint only when one of its constituents is instantiable, or is // an intersection that might contain one, so the walk is skipped for unions with neither. func (c *Checker) someGenericTypeWithUnionConstraint(t *Type) bool { - if t.flags&TypeFlagsUnion != 0 && t.objectFlags&(ObjectFlagsContainsInstantiables|ObjectFlagsContainsIntersections) == 0 { + if t.flags&TypeFlagsUnion != 0 && t.objectFlags&ObjectFlagsContainsIntersections == 0 && !t.AsUnionType().containsInstantiables { return false } return someType(t, c.isGenericTypeWithUnionConstraint) diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index cfdef60b0f684..67072201e0262 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -726,7 +726,6 @@ const ( ObjectFlagsIsUnknownLikeUnion = 1 << 27 // Union of null, undefined, and empty object type ObjectFlagsIsUniformEnumComputed = 1 << 28 // IsUniformEnum flag has been computed ObjectFlagsIsUniformEnum = 1 << 29 // Union contains uniform literal types - ObjectFlagsContainsInstantiables = 1 << 30 // Union contains instantiable types // Flags that require TypeFlags.Intersection ObjectFlagsIsNeverIntersectionComputed = 1 << 25 // IsNeverLike flag has been computed ObjectFlagsIsNeverIntersection = 1 << 26 // Intersection reduces to never @@ -1475,11 +1474,12 @@ func (t *UnionOrIntersectionType) Types() []*Type { type UnionType struct { UnionOrIntersectionType - resolvedReducedType *Type - regularType *Type - origin *Type // Denormalized union, intersection, or index type in which union originates - keyPropertyName string // Property with unique unit type that exists in every object/intersection in union type - constituentMap map[*Type]*Type // Constituents keyed by unit type discriminants + resolvedReducedType *Type + regularType *Type + origin *Type // Denormalized union, intersection, or index type in which union originates + keyPropertyName string // Property with unique unit type that exists in every object/intersection in union type + constituentMap map[*Type]*Type // Constituents keyed by unit type discriminants + containsInstantiables bool // Union contains instantiable types } // IntersectionType