Skip to content

Commit d41943e

Browse files
authored
Properly handle tagged primitives in control flow analysis (#43538)
* Ignore object types in intersections with primitive types * Add regression test * Also handle instantiable types constrained to object types * Add another test * Add ignoreObjects optional parameter to getTypeFacts
1 parent a4c683b commit d41943e

File tree

5 files changed

+158
-5
lines changed

5 files changed

+158
-5
lines changed

src/compiler/checker.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21965,7 +21965,7 @@ namespace ts {
2196521965
resolved.members.get("bind" as __String) && isTypeSubtypeOf(type, globalFunctionType));
2196621966
}
2196721967

21968-
function getTypeFacts(type: Type): TypeFacts {
21968+
function getTypeFacts(type: Type, ignoreObjects = false): TypeFacts {
2196921969
const flags = type.flags;
2197021970
if (flags & TypeFlags.String) {
2197121971
return strictNullChecks ? TypeFacts.StringStrictFacts : TypeFacts.StringFacts;
@@ -22002,7 +22002,7 @@ namespace ts {
2200222002
(type === falseType || type === regularFalseType) ? TypeFacts.FalseStrictFacts : TypeFacts.TrueStrictFacts :
2200322003
(type === falseType || type === regularFalseType) ? TypeFacts.FalseFacts : TypeFacts.TrueFacts;
2200422004
}
22005-
if (flags & TypeFlags.Object) {
22005+
if (flags & TypeFlags.Object && !ignoreObjects) {
2200622006
return getObjectFlags(type) & ObjectFlags.Anonymous && isEmptyObjectType(<ObjectType>type) ?
2200722007
strictNullChecks ? TypeFacts.EmptyObjectStrictFacts : TypeFacts.EmptyObjectFacts :
2200822008
isFunctionObjectType(<ObjectType>type) ?
@@ -22025,14 +22025,17 @@ namespace ts {
2202522025
return TypeFacts.None;
2202622026
}
2202722027
if (flags & TypeFlags.Instantiable) {
22028-
return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType) :
22028+
return !isPatternLiteralType(type) ? getTypeFacts(getBaseConstraintOfType(type) || unknownType, ignoreObjects) :
2202922029
strictNullChecks ? TypeFacts.NonEmptyStringStrictFacts : TypeFacts.NonEmptyStringFacts;
2203022030
}
2203122031
if (flags & TypeFlags.Union) {
22032-
return reduceLeft((<UnionType>type).types, (facts, t) => facts | getTypeFacts(t), TypeFacts.None);
22032+
return reduceLeft((<UnionType>type).types, (facts, t) => facts | getTypeFacts(t, ignoreObjects), TypeFacts.None);
2203322033
}
2203422034
if (flags & TypeFlags.Intersection) {
22035-
return reduceLeft((<UnionType>type).types, (facts, t) => facts & getTypeFacts(t), TypeFacts.All);
22035+
// When an intersection contains a primitive type we ignore object type constituents as they are
22036+
// presumably type tags. For example, in string & { __kind__: "name" } we ignore the object type.
22037+
ignoreObjects ||= maybeTypeOfKind(type, TypeFlags.Primitive);
22038+
return reduceLeft((<UnionType>type).types, (facts, t) => facts & getTypeFacts(t, ignoreObjects), TypeFacts.All);
2203622039
}
2203722040
return TypeFacts.All;
2203822041
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//// [taggedPrimitiveNarrowing.ts]
2+
type Hash = string & { __hash: true };
3+
4+
function getHashLength(hash: Hash): number {
5+
if (typeof hash !== "string") {
6+
throw new Error("This doesn't look like a hash");
7+
}
8+
return hash.length;
9+
}
10+
11+
function getHashLength2<T extends { __tag__: unknown}>(hash: string & T): number {
12+
if (typeof hash !== "string") {
13+
throw new Error("This doesn't look like a hash");
14+
}
15+
return hash.length;
16+
}
17+
18+
19+
//// [taggedPrimitiveNarrowing.js]
20+
"use strict";
21+
function getHashLength(hash) {
22+
if (typeof hash !== "string") {
23+
throw new Error("This doesn't look like a hash");
24+
}
25+
return hash.length;
26+
}
27+
function getHashLength2(hash) {
28+
if (typeof hash !== "string") {
29+
throw new Error("This doesn't look like a hash");
30+
}
31+
return hash.length;
32+
}
33+
34+
35+
//// [taggedPrimitiveNarrowing.d.ts]
36+
declare type Hash = string & {
37+
__hash: true;
38+
};
39+
declare function getHashLength(hash: Hash): number;
40+
declare function getHashLength2<T extends {
41+
__tag__: unknown;
42+
}>(hash: string & T): number;
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
=== tests/cases/compiler/taggedPrimitiveNarrowing.ts ===
2+
type Hash = string & { __hash: true };
3+
>Hash : Symbol(Hash, Decl(taggedPrimitiveNarrowing.ts, 0, 0))
4+
>__hash : Symbol(__hash, Decl(taggedPrimitiveNarrowing.ts, 0, 22))
5+
6+
function getHashLength(hash: Hash): number {
7+
>getHashLength : Symbol(getHashLength, Decl(taggedPrimitiveNarrowing.ts, 0, 38))
8+
>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 2, 23))
9+
>Hash : Symbol(Hash, Decl(taggedPrimitiveNarrowing.ts, 0, 0))
10+
11+
if (typeof hash !== "string") {
12+
>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 2, 23))
13+
14+
throw new Error("This doesn't look like a hash");
15+
>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
16+
}
17+
return hash.length;
18+
>hash.length : Symbol(String.length, Decl(lib.es5.d.ts, --, --))
19+
>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 2, 23))
20+
>length : Symbol(String.length, Decl(lib.es5.d.ts, --, --))
21+
}
22+
23+
function getHashLength2<T extends { __tag__: unknown}>(hash: string & T): number {
24+
>getHashLength2 : Symbol(getHashLength2, Decl(taggedPrimitiveNarrowing.ts, 7, 1))
25+
>T : Symbol(T, Decl(taggedPrimitiveNarrowing.ts, 9, 24))
26+
>__tag__ : Symbol(__tag__, Decl(taggedPrimitiveNarrowing.ts, 9, 35))
27+
>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 9, 55))
28+
>T : Symbol(T, Decl(taggedPrimitiveNarrowing.ts, 9, 24))
29+
30+
if (typeof hash !== "string") {
31+
>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 9, 55))
32+
33+
throw new Error("This doesn't look like a hash");
34+
>Error : Symbol(Error, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
35+
}
36+
return hash.length;
37+
>hash.length : Symbol(String.length, Decl(lib.es5.d.ts, --, --))
38+
>hash : Symbol(hash, Decl(taggedPrimitiveNarrowing.ts, 9, 55))
39+
>length : Symbol(String.length, Decl(lib.es5.d.ts, --, --))
40+
}
41+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
=== tests/cases/compiler/taggedPrimitiveNarrowing.ts ===
2+
type Hash = string & { __hash: true };
3+
>Hash : Hash
4+
>__hash : true
5+
>true : true
6+
7+
function getHashLength(hash: Hash): number {
8+
>getHashLength : (hash: Hash) => number
9+
>hash : Hash
10+
11+
if (typeof hash !== "string") {
12+
>typeof hash !== "string" : boolean
13+
>typeof hash : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
14+
>hash : Hash
15+
>"string" : "string"
16+
17+
throw new Error("This doesn't look like a hash");
18+
>new Error("This doesn't look like a hash") : Error
19+
>Error : ErrorConstructor
20+
>"This doesn't look like a hash" : "This doesn't look like a hash"
21+
}
22+
return hash.length;
23+
>hash.length : number
24+
>hash : Hash
25+
>length : number
26+
}
27+
28+
function getHashLength2<T extends { __tag__: unknown}>(hash: string & T): number {
29+
>getHashLength2 : <T extends { __tag__: unknown; }>(hash: string & T) => number
30+
>__tag__ : unknown
31+
>hash : string & T
32+
33+
if (typeof hash !== "string") {
34+
>typeof hash !== "string" : boolean
35+
>typeof hash : "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"
36+
>hash : string & T
37+
>"string" : "string"
38+
39+
throw new Error("This doesn't look like a hash");
40+
>new Error("This doesn't look like a hash") : Error
41+
>Error : ErrorConstructor
42+
>"This doesn't look like a hash" : "This doesn't look like a hash"
43+
}
44+
return hash.length;
45+
>hash.length : number
46+
>hash : string & T
47+
>length : number
48+
}
49+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @strict: true
2+
// @declaration: true
3+
4+
type Hash = string & { __hash: true };
5+
6+
function getHashLength(hash: Hash): number {
7+
if (typeof hash !== "string") {
8+
throw new Error("This doesn't look like a hash");
9+
}
10+
return hash.length;
11+
}
12+
13+
function getHashLength2<T extends { __tag__: unknown}>(hash: string & T): number {
14+
if (typeof hash !== "string") {
15+
throw new Error("This doesn't look like a hash");
16+
}
17+
return hash.length;
18+
}

0 commit comments

Comments
 (0)