Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand Down
11 changes: 6 additions & 5 deletions tsc/internal/checker/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string> 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<T> {
>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<string>;
>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))
}
}

Original file line number Diff line number Diff line change
@@ -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<string> 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<T> {
u: U2;
>u : U2

t: T;
>t : T
}

declare const box: Box<string>;
>box : Box<string>

box.u;
>box.u : U2
>box : Box<string>
>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
}
}

Original file line number Diff line number Diff line change
@@ -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<string> has computed whether U2 could contain type variables.

type U2 = NoInfer<{ kind: "a"; a: number } | { kind: "b"; b: number }> | { kind: "c" };

interface Box<T> {
u: U2;
t: T;
}

declare const box: Box<string>;
box.u;

function f2(u: U2) {
if (u.kind === "a") {
u.a;
}
}