diff --git a/tsc/internal/checker/checker.go b/tsc/internal/checker/checker.go index 4c57dd37910f2..e15d67d9f69bb 100644 --- a/tsc/internal/checker/checker.go +++ b/tsc/internal/checker/checker.go @@ -26471,6 +26471,7 @@ func (c *Checker) getUnionTypeFromSortedList(types []*Type, precomputedObjectFla t := c.unionTypes[key] if t == nil { 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 { @@ -32269,7 +32270,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 +32287,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 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&ObjectFlagsContainsIntersections == 0 && !t.AsUnionType().containsInstantiables { + 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) diff --git a/tsc/internal/checker/types.go b/tsc/internal/checker/types.go index f119288a21744..67072201e0262 100644 --- a/tsc/internal/checker/types.go +++ b/tsc/internal/checker/types.go @@ -1474,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 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; + } +}