Skip to content

Commit 667f3b4

Browse files
authored
Allow assertion signatures to narrow by discriminant (#37310)
1 parent ac3dc0c commit 667f3b4

5 files changed

+204
-1
lines changed

src/compiler/checker.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20641,7 +20641,10 @@ namespace ts {
2064120641
}
2064220642
if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) &&
2064320643
!(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) {
20644-
return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
20644+
type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull);
20645+
}
20646+
if (isMatchingReferenceDiscriminant(predicateArgument, declaredType)) {
20647+
return narrowTypeByDiscriminant(type, predicateArgument as AccessExpression, t => getNarrowedType(t, predicate.type!, assumeTrue, isTypeSubtypeOf));
2064520648
}
2064620649
}
2064720650
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//// [assertionFunctionsCanNarrowByDiscriminant.ts]
2+
interface Cat {
3+
type: 'cat';
4+
canMeow: true;
5+
}
6+
7+
interface Dog {
8+
type: 'dog';
9+
canBark: true;
10+
}
11+
12+
type Animal = Cat | Dog;
13+
14+
declare function assertEqual<T>(value: any, type: T): asserts value is T;
15+
16+
const animal = { type: 'cat', canMeow: true } as Animal;
17+
assertEqual(animal.type, 'cat' as const);
18+
19+
animal.canMeow; // since is cat, should not be an error
20+
21+
const animalOrUndef = { type: 'cat', canMeow: true } as Animal | undefined;
22+
assertEqual(animalOrUndef?.type, 'cat' as const);
23+
24+
animalOrUndef.canMeow; // since is cat, should not be an error
25+
26+
27+
//// [assertionFunctionsCanNarrowByDiscriminant.js]
28+
"use strict";
29+
var animal = { type: 'cat', canMeow: true };
30+
assertEqual(animal.type, 'cat');
31+
animal.canMeow; // since is cat, should not be an error
32+
var animalOrUndef = { type: 'cat', canMeow: true };
33+
assertEqual(animalOrUndef === null || animalOrUndef === void 0 ? void 0 : animalOrUndef.type, 'cat');
34+
animalOrUndef.canMeow; // since is cat, should not be an error
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
=== tests/cases/compiler/assertionFunctionsCanNarrowByDiscriminant.ts ===
2+
interface Cat {
3+
>Cat : Symbol(Cat, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 0))
4+
5+
type: 'cat';
6+
>type : Symbol(Cat.type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15))
7+
8+
canMeow: true;
9+
>canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16))
10+
}
11+
12+
interface Dog {
13+
>Dog : Symbol(Dog, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 3, 1))
14+
15+
type: 'dog';
16+
>type : Symbol(Dog.type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15))
17+
18+
canBark: true;
19+
>canBark : Symbol(Dog.canBark, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 6, 16))
20+
}
21+
22+
type Animal = Cat | Dog;
23+
>Animal : Symbol(Animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 8, 1))
24+
>Cat : Symbol(Cat, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 0))
25+
>Dog : Symbol(Dog, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 3, 1))
26+
27+
declare function assertEqual<T>(value: any, type: T): asserts value is T;
28+
>assertEqual : Symbol(assertEqual, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 10, 24))
29+
>T : Symbol(T, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 29))
30+
>value : Symbol(value, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 32))
31+
>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 43))
32+
>T : Symbol(T, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 29))
33+
>value : Symbol(value, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 32))
34+
>T : Symbol(T, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 29))
35+
36+
const animal = { type: 'cat', canMeow: true } as Animal;
37+
>animal : Symbol(animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 5))
38+
>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 16))
39+
>canMeow : Symbol(canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 29))
40+
>Animal : Symbol(Animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 8, 1))
41+
42+
assertEqual(animal.type, 'cat' as const);
43+
>assertEqual : Symbol(assertEqual, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 10, 24))
44+
>animal.type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15), Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15))
45+
>animal : Symbol(animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 5))
46+
>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15), Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15))
47+
48+
animal.canMeow; // since is cat, should not be an error
49+
>animal.canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16))
50+
>animal : Symbol(animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 5))
51+
>canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16))
52+
53+
const animalOrUndef = { type: 'cat', canMeow: true } as Animal | undefined;
54+
>animalOrUndef : Symbol(animalOrUndef, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 5))
55+
>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 23))
56+
>canMeow : Symbol(canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 36))
57+
>Animal : Symbol(Animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 8, 1))
58+
59+
assertEqual(animalOrUndef?.type, 'cat' as const);
60+
>assertEqual : Symbol(assertEqual, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 10, 24))
61+
>animalOrUndef?.type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15), Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15))
62+
>animalOrUndef : Symbol(animalOrUndef, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 5))
63+
>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15), Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15))
64+
65+
animalOrUndef.canMeow; // since is cat, should not be an error
66+
>animalOrUndef.canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16))
67+
>animalOrUndef : Symbol(animalOrUndef, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 5))
68+
>canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16))
69+
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
=== tests/cases/compiler/assertionFunctionsCanNarrowByDiscriminant.ts ===
2+
interface Cat {
3+
type: 'cat';
4+
>type : "cat"
5+
6+
canMeow: true;
7+
>canMeow : true
8+
>true : true
9+
}
10+
11+
interface Dog {
12+
type: 'dog';
13+
>type : "dog"
14+
15+
canBark: true;
16+
>canBark : true
17+
>true : true
18+
}
19+
20+
type Animal = Cat | Dog;
21+
>Animal : Animal
22+
23+
declare function assertEqual<T>(value: any, type: T): asserts value is T;
24+
>assertEqual : <T>(value: any, type: T) => asserts value is T
25+
>value : any
26+
>type : T
27+
28+
const animal = { type: 'cat', canMeow: true } as Animal;
29+
>animal : Animal
30+
>{ type: 'cat', canMeow: true } as Animal : Animal
31+
>{ type: 'cat', canMeow: true } : { type: "cat"; canMeow: true; }
32+
>type : "cat"
33+
>'cat' : "cat"
34+
>canMeow : true
35+
>true : true
36+
37+
assertEqual(animal.type, 'cat' as const);
38+
>assertEqual(animal.type, 'cat' as const) : void
39+
>assertEqual : <T>(value: any, type: T) => asserts value is T
40+
>animal.type : "cat" | "dog"
41+
>animal : Animal
42+
>type : "cat" | "dog"
43+
>'cat' as const : "cat"
44+
>'cat' : "cat"
45+
46+
animal.canMeow; // since is cat, should not be an error
47+
>animal.canMeow : true
48+
>animal : Cat
49+
>canMeow : true
50+
51+
const animalOrUndef = { type: 'cat', canMeow: true } as Animal | undefined;
52+
>animalOrUndef : Cat | Dog | undefined
53+
>{ type: 'cat', canMeow: true } as Animal | undefined : Cat | Dog | undefined
54+
>{ type: 'cat', canMeow: true } : { type: "cat"; canMeow: true; }
55+
>type : "cat"
56+
>'cat' : "cat"
57+
>canMeow : true
58+
>true : true
59+
60+
assertEqual(animalOrUndef?.type, 'cat' as const);
61+
>assertEqual(animalOrUndef?.type, 'cat' as const) : void
62+
>assertEqual : <T>(value: any, type: T) => asserts value is T
63+
>animalOrUndef?.type : "cat" | "dog" | undefined
64+
>animalOrUndef : Cat | Dog | undefined
65+
>type : "cat" | "dog" | undefined
66+
>'cat' as const : "cat"
67+
>'cat' : "cat"
68+
69+
animalOrUndef.canMeow; // since is cat, should not be an error
70+
>animalOrUndef.canMeow : true
71+
>animalOrUndef : Cat
72+
>canMeow : true
73+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// @strict: true
2+
interface Cat {
3+
type: 'cat';
4+
canMeow: true;
5+
}
6+
7+
interface Dog {
8+
type: 'dog';
9+
canBark: true;
10+
}
11+
12+
type Animal = Cat | Dog;
13+
14+
declare function assertEqual<T>(value: any, type: T): asserts value is T;
15+
16+
const animal = { type: 'cat', canMeow: true } as Animal;
17+
assertEqual(animal.type, 'cat' as const);
18+
19+
animal.canMeow; // since is cat, should not be an error
20+
21+
const animalOrUndef = { type: 'cat', canMeow: true } as Animal | undefined;
22+
assertEqual(animalOrUndef?.type, 'cat' as const);
23+
24+
animalOrUndef.canMeow; // since is cat, should not be an error

0 commit comments

Comments
 (0)