diff --git a/tsc/internal/checker/flow.go b/tsc/internal/checker/flow.go index d08254cc8ef88..e5d6e2fb09ec4 100644 --- a/tsc/internal/checker/flow.go +++ b/tsc/internal/checker/flow.go @@ -21,6 +21,11 @@ type FlowType struct { incomplete bool } +type literalTypeKey struct { + flags TypeFlags + value any +} + func (ft *FlowType) isNil() bool { return ft.t == nil } @@ -880,6 +885,11 @@ func (c *Checker) getNarrowedTypeWorker(t *Type, candidate *Type, assumeTrue boo if t == candidate { return candidate } + if !checkDerived { + if narrowedType := c.tryNarrowLiteralUnion(t, candidate); narrowedType != nil { + return narrowedType + } + } // We first attempt to filter the current type, narrowing constituents as appropriate and removing // constituents that are unrelated to the candidate. var keyPropertyName string @@ -913,6 +923,17 @@ func (c *Checker) getNarrowedTypeWorker(t *Type, candidate *Type, assumeTrue boo } } else { mapType = func(t *Type) *Type { + tLiteralFlags := t.flags & TypeFlagsLiteral + nLiteralFlags := n.flags & TypeFlagsLiteral + if tLiteralFlags != 0 && tLiteralFlags == nLiteralFlags && !(t.flags&TypeFlagsEnumLiteral != 0 && n.flags&TypeFlagsEnumLiteral != 0) { + if t.AsLiteralType().value == n.AsLiteralType().value { + if n.flags&TypeFlagsEnumLiteral != 0 { + return n + } + return t + } + return c.neverType + } switch { case c.isTypeStrictSubtypeOf(t, n): return t @@ -963,6 +984,23 @@ func (c *Checker) getNarrowedTypeWorker(t *Type, candidate *Type, assumeTrue boo return c.getIntersectionType([]*Type{t, candidate}) } +func (c *Checker) tryNarrowLiteralUnion(t *Type, candidate *Type) *Type { + isNonEnumLiteral := func(t *Type) bool { + return t.flags&TypeFlagsLiteral != 0 && t.flags&TypeFlagsEnumLiteral == 0 + } + if !everyType(t, isNonEnumLiteral) || !everyType(candidate, isNonEnumLiteral) { + return nil + } + candidateTypes := make(map[literalTypeKey]struct{}) + forEachType(candidate, func(t *Type) { + candidateTypes[literalTypeKey{flags: t.flags & TypeFlagsLiteral, value: t.AsLiteralType().value}] = struct{}{} + }) + return c.filterType(t, func(t *Type) bool { + _, ok := candidateTypes[literalTypeKey{flags: t.flags & TypeFlagsLiteral, value: t.AsLiteralType().value}] + return ok + }) +} + func (c *Checker) getInstanceType(constructorType *Type) *Type { prototypePropertyType := c.getTypeOfPropertyOfType(constructorType, "prototype") if prototypePropertyType != nil && !IsTypeAny(prototypePropertyType) { diff --git a/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.js b/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.js new file mode 100644 index 0000000000000..43ae6d32f0bd3 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.js @@ -0,0 +1,39 @@ +//// [tests/cases/compiler/narrowLiteralUnionByTypePredicate.ts] //// + +//// [narrowLiteralUnionByTypePredicate.ts] +type Source = "a" | "b" | "c" | "d" | 1 | 2 | 3 | 4; +type Candidate = "b" | "d" | 1 | 3; + +declare const source: Source; +declare function isCandidate(value: Source): value is Candidate; + +if (isCandidate(source)) { + source; +} + +enum E { + A = "a", + B = "b", +} + +declare const enumSource: "a" | "b"; +declare function isEnumA(value: "a" | "b"): value is E.A; + +if (isEnumA(enumSource)) { + const enumA: E.A = enumSource; +} + + +//// [narrowLiteralUnionByTypePredicate.js] +"use strict"; +if (isCandidate(source)) { + source; +} +var E; +(function (E) { + E["A"] = "a"; + E["B"] = "b"; +})(E || (E = {})); +if (isEnumA(enumSource)) { + const enumA = enumSource; +} diff --git a/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.symbols b/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.symbols new file mode 100644 index 0000000000000..570ee92c47fbb --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.symbols @@ -0,0 +1,59 @@ +//// [tests/cases/compiler/narrowLiteralUnionByTypePredicate.ts] //// + +=== narrowLiteralUnionByTypePredicate.ts === +type Source = "a" | "b" | "c" | "d" | 1 | 2 | 3 | 4; +>Source : Symbol(Source, Decl(narrowLiteralUnionByTypePredicate.ts, 0, 0)) + +type Candidate = "b" | "d" | 1 | 3; +>Candidate : Symbol(Candidate, Decl(narrowLiteralUnionByTypePredicate.ts, 0, 52)) + +declare const source: Source; +>source : Symbol(source, Decl(narrowLiteralUnionByTypePredicate.ts, 3, 13)) +>Source : Symbol(Source, Decl(narrowLiteralUnionByTypePredicate.ts, 0, 0)) + +declare function isCandidate(value: Source): value is Candidate; +>isCandidate : Symbol(isCandidate, Decl(narrowLiteralUnionByTypePredicate.ts, 3, 29)) +>value : Symbol(value, Decl(narrowLiteralUnionByTypePredicate.ts, 4, 29)) +>Source : Symbol(Source, Decl(narrowLiteralUnionByTypePredicate.ts, 0, 0)) +>value : Symbol(value, Decl(narrowLiteralUnionByTypePredicate.ts, 4, 29)) +>Candidate : Symbol(Candidate, Decl(narrowLiteralUnionByTypePredicate.ts, 0, 52)) + +if (isCandidate(source)) { +>isCandidate : Symbol(isCandidate, Decl(narrowLiteralUnionByTypePredicate.ts, 3, 29)) +>source : Symbol(source, Decl(narrowLiteralUnionByTypePredicate.ts, 3, 13)) + + source; +>source : Symbol(source, Decl(narrowLiteralUnionByTypePredicate.ts, 3, 13)) +} + +enum E { +>E : Symbol(E, Decl(narrowLiteralUnionByTypePredicate.ts, 8, 1)) + + A = "a", +>A : Symbol(E.A, Decl(narrowLiteralUnionByTypePredicate.ts, 10, 8)) + + B = "b", +>B : Symbol(E.B, Decl(narrowLiteralUnionByTypePredicate.ts, 11, 12)) +} + +declare const enumSource: "a" | "b"; +>enumSource : Symbol(enumSource, Decl(narrowLiteralUnionByTypePredicate.ts, 15, 13)) + +declare function isEnumA(value: "a" | "b"): value is E.A; +>isEnumA : Symbol(isEnumA, Decl(narrowLiteralUnionByTypePredicate.ts, 15, 36)) +>value : Symbol(value, Decl(narrowLiteralUnionByTypePredicate.ts, 16, 25)) +>value : Symbol(value, Decl(narrowLiteralUnionByTypePredicate.ts, 16, 25)) +>E : Symbol(E, Decl(narrowLiteralUnionByTypePredicate.ts, 8, 1)) +>A : Symbol(E.A, Decl(narrowLiteralUnionByTypePredicate.ts, 10, 8)) + +if (isEnumA(enumSource)) { +>isEnumA : Symbol(isEnumA, Decl(narrowLiteralUnionByTypePredicate.ts, 15, 36)) +>enumSource : Symbol(enumSource, Decl(narrowLiteralUnionByTypePredicate.ts, 15, 13)) + + const enumA: E.A = enumSource; +>enumA : Symbol(enumA, Decl(narrowLiteralUnionByTypePredicate.ts, 19, 9)) +>E : Symbol(E, Decl(narrowLiteralUnionByTypePredicate.ts, 8, 1)) +>A : Symbol(E.A, Decl(narrowLiteralUnionByTypePredicate.ts, 10, 8)) +>enumSource : Symbol(enumSource, Decl(narrowLiteralUnionByTypePredicate.ts, 15, 13)) +} + diff --git a/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.types b/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.types new file mode 100644 index 0000000000000..f8b7188f14618 --- /dev/null +++ b/tsc/testdata/baselines/reference/compiler/narrowLiteralUnionByTypePredicate.types @@ -0,0 +1,56 @@ +//// [tests/cases/compiler/narrowLiteralUnionByTypePredicate.ts] //// + +=== narrowLiteralUnionByTypePredicate.ts === +type Source = "a" | "b" | "c" | "d" | 1 | 2 | 3 | 4; +>Source : Source + +type Candidate = "b" | "d" | 1 | 3; +>Candidate : Candidate + +declare const source: Source; +>source : Source + +declare function isCandidate(value: Source): value is Candidate; +>isCandidate : (value: Source) => value is Candidate +>value : Source + +if (isCandidate(source)) { +>isCandidate(source) : boolean +>isCandidate : (value: Source) => value is Candidate +>source : Source + + source; +>source : "b" | "d" | 1 | 3 +} + +enum E { +>E : E + + A = "a", +>A : E.A +>"a" : "a" + + B = "b", +>B : E.B +>"b" : "b" +} + +declare const enumSource: "a" | "b"; +>enumSource : "a" | "b" + +declare function isEnumA(value: "a" | "b"): value is E.A; +>isEnumA : (value: "a" | "b") => value is E.A +>value : "a" | "b" +>E : any + +if (isEnumA(enumSource)) { +>isEnumA(enumSource) : boolean +>isEnumA : (value: "a" | "b") => value is E.A +>enumSource : "a" | "b" + + const enumA: E.A = enumSource; +>enumA : E.A +>E : any +>enumSource : E.A +} + diff --git a/tsc/testdata/tests/cases/compiler/narrowLiteralUnionByTypePredicate.ts b/tsc/testdata/tests/cases/compiler/narrowLiteralUnionByTypePredicate.ts new file mode 100644 index 0000000000000..3bdd49ee38b52 --- /dev/null +++ b/tsc/testdata/tests/cases/compiler/narrowLiteralUnionByTypePredicate.ts @@ -0,0 +1,23 @@ +// @strict: true + +type Source = "a" | "b" | "c" | "d" | 1 | 2 | 3 | 4; +type Candidate = "b" | "d" | 1 | 3; + +declare const source: Source; +declare function isCandidate(value: Source): value is Candidate; + +if (isCandidate(source)) { + source; +} + +enum E { + A = "a", + B = "b", +} + +declare const enumSource: "a" | "b"; +declare function isEnumA(value: "a" | "b"): value is E.A; + +if (isEnumA(enumSource)) { + const enumA: E.A = enumSource; +}