From 259e4a37ab3d8b41026721b05a33ac7cd8ebd718 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Mon, 9 Mar 2020 16:07:10 -0700 Subject: [PATCH] Allow assertion signatures to narrow by discriminant --- src/compiler/checker.ts | 5 +- ...sertionFunctionsCanNarrowByDiscriminant.js | 34 +++++++++ ...onFunctionsCanNarrowByDiscriminant.symbols | 69 ++++++++++++++++++ ...tionFunctionsCanNarrowByDiscriminant.types | 73 +++++++++++++++++++ ...sertionFunctionsCanNarrowByDiscriminant.ts | 24 ++++++ 5 files changed, 204 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.js create mode 100644 tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.symbols create mode 100644 tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.types create mode 100644 tests/cases/compiler/assertionFunctionsCanNarrowByDiscriminant.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 645bf049f4e12..54d74dda73b19 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -20507,7 +20507,10 @@ namespace ts { } if (strictNullChecks && assumeTrue && optionalChainContainsReference(predicateArgument, reference) && !(getTypeFacts(predicate.type) & TypeFacts.EQUndefined)) { - return getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); + type = getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull); + } + if (isMatchingReferenceDiscriminant(predicateArgument, declaredType)) { + return narrowTypeByDiscriminant(type, predicateArgument as AccessExpression, t => getNarrowedType(t, predicate.type!, assumeTrue, isTypeSubtypeOf)); } } } diff --git a/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.js b/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.js new file mode 100644 index 0000000000000..8857b7dd16013 --- /dev/null +++ b/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.js @@ -0,0 +1,34 @@ +//// [assertionFunctionsCanNarrowByDiscriminant.ts] +interface Cat { + type: 'cat'; + canMeow: true; +} + +interface Dog { + type: 'dog'; + canBark: true; +} + +type Animal = Cat | Dog; + +declare function assertEqual(value: any, type: T): asserts value is T; + +const animal = { type: 'cat', canMeow: true } as Animal; +assertEqual(animal.type, 'cat' as const); + +animal.canMeow; // since is cat, should not be an error + +const animalOrUndef = { type: 'cat', canMeow: true } as Animal | undefined; +assertEqual(animalOrUndef?.type, 'cat' as const); + +animalOrUndef.canMeow; // since is cat, should not be an error + + +//// [assertionFunctionsCanNarrowByDiscriminant.js] +"use strict"; +var animal = { type: 'cat', canMeow: true }; +assertEqual(animal.type, 'cat'); +animal.canMeow; // since is cat, should not be an error +var animalOrUndef = { type: 'cat', canMeow: true }; +assertEqual(animalOrUndef === null || animalOrUndef === void 0 ? void 0 : animalOrUndef.type, 'cat'); +animalOrUndef.canMeow; // since is cat, should not be an error diff --git a/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.symbols b/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.symbols new file mode 100644 index 0000000000000..4df3e8e983672 --- /dev/null +++ b/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.symbols @@ -0,0 +1,69 @@ +=== tests/cases/compiler/assertionFunctionsCanNarrowByDiscriminant.ts === +interface Cat { +>Cat : Symbol(Cat, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 0)) + + type: 'cat'; +>type : Symbol(Cat.type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15)) + + canMeow: true; +>canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16)) +} + +interface Dog { +>Dog : Symbol(Dog, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 3, 1)) + + type: 'dog'; +>type : Symbol(Dog.type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15)) + + canBark: true; +>canBark : Symbol(Dog.canBark, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 6, 16)) +} + +type Animal = Cat | Dog; +>Animal : Symbol(Animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 8, 1)) +>Cat : Symbol(Cat, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 0)) +>Dog : Symbol(Dog, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 3, 1)) + +declare function assertEqual(value: any, type: T): asserts value is T; +>assertEqual : Symbol(assertEqual, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 10, 24)) +>T : Symbol(T, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 29)) +>value : Symbol(value, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 32)) +>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 43)) +>T : Symbol(T, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 29)) +>value : Symbol(value, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 32)) +>T : Symbol(T, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 12, 29)) + +const animal = { type: 'cat', canMeow: true } as Animal; +>animal : Symbol(animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 5)) +>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 16)) +>canMeow : Symbol(canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 29)) +>Animal : Symbol(Animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 8, 1)) + +assertEqual(animal.type, 'cat' as const); +>assertEqual : Symbol(assertEqual, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 10, 24)) +>animal.type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15), Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15)) +>animal : Symbol(animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 5)) +>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15), Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15)) + +animal.canMeow; // since is cat, should not be an error +>animal.canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16)) +>animal : Symbol(animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 14, 5)) +>canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16)) + +const animalOrUndef = { type: 'cat', canMeow: true } as Animal | undefined; +>animalOrUndef : Symbol(animalOrUndef, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 5)) +>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 23)) +>canMeow : Symbol(canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 36)) +>Animal : Symbol(Animal, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 8, 1)) + +assertEqual(animalOrUndef?.type, 'cat' as const); +>assertEqual : Symbol(assertEqual, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 10, 24)) +>animalOrUndef?.type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15), Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15)) +>animalOrUndef : Symbol(animalOrUndef, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 5)) +>type : Symbol(type, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 0, 15), Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 5, 15)) + +animalOrUndef.canMeow; // since is cat, should not be an error +>animalOrUndef.canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16)) +>animalOrUndef : Symbol(animalOrUndef, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 19, 5)) +>canMeow : Symbol(Cat.canMeow, Decl(assertionFunctionsCanNarrowByDiscriminant.ts, 1, 16)) + diff --git a/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.types b/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.types new file mode 100644 index 0000000000000..9de2392fe058c --- /dev/null +++ b/tests/baselines/reference/assertionFunctionsCanNarrowByDiscriminant.types @@ -0,0 +1,73 @@ +=== tests/cases/compiler/assertionFunctionsCanNarrowByDiscriminant.ts === +interface Cat { + type: 'cat'; +>type : "cat" + + canMeow: true; +>canMeow : true +>true : true +} + +interface Dog { + type: 'dog'; +>type : "dog" + + canBark: true; +>canBark : true +>true : true +} + +type Animal = Cat | Dog; +>Animal : Animal + +declare function assertEqual(value: any, type: T): asserts value is T; +>assertEqual : (value: any, type: T) => asserts value is T +>value : any +>type : T + +const animal = { type: 'cat', canMeow: true } as Animal; +>animal : Animal +>{ type: 'cat', canMeow: true } as Animal : Animal +>{ type: 'cat', canMeow: true } : { type: "cat"; canMeow: true; } +>type : "cat" +>'cat' : "cat" +>canMeow : true +>true : true + +assertEqual(animal.type, 'cat' as const); +>assertEqual(animal.type, 'cat' as const) : void +>assertEqual : (value: any, type: T) => asserts value is T +>animal.type : "cat" | "dog" +>animal : Animal +>type : "cat" | "dog" +>'cat' as const : "cat" +>'cat' : "cat" + +animal.canMeow; // since is cat, should not be an error +>animal.canMeow : true +>animal : Cat +>canMeow : true + +const animalOrUndef = { type: 'cat', canMeow: true } as Animal | undefined; +>animalOrUndef : Cat | Dog | undefined +>{ type: 'cat', canMeow: true } as Animal | undefined : Cat | Dog | undefined +>{ type: 'cat', canMeow: true } : { type: "cat"; canMeow: true; } +>type : "cat" +>'cat' : "cat" +>canMeow : true +>true : true + +assertEqual(animalOrUndef?.type, 'cat' as const); +>assertEqual(animalOrUndef?.type, 'cat' as const) : void +>assertEqual : (value: any, type: T) => asserts value is T +>animalOrUndef?.type : "cat" | "dog" | undefined +>animalOrUndef : Cat | Dog | undefined +>type : "cat" | "dog" | undefined +>'cat' as const : "cat" +>'cat' : "cat" + +animalOrUndef.canMeow; // since is cat, should not be an error +>animalOrUndef.canMeow : true +>animalOrUndef : Cat +>canMeow : true + diff --git a/tests/cases/compiler/assertionFunctionsCanNarrowByDiscriminant.ts b/tests/cases/compiler/assertionFunctionsCanNarrowByDiscriminant.ts new file mode 100644 index 0000000000000..79583a3aa9ce9 --- /dev/null +++ b/tests/cases/compiler/assertionFunctionsCanNarrowByDiscriminant.ts @@ -0,0 +1,24 @@ +// @strict: true +interface Cat { + type: 'cat'; + canMeow: true; +} + +interface Dog { + type: 'dog'; + canBark: true; +} + +type Animal = Cat | Dog; + +declare function assertEqual(value: any, type: T): asserts value is T; + +const animal = { type: 'cat', canMeow: true } as Animal; +assertEqual(animal.type, 'cat' as const); + +animal.canMeow; // since is cat, should not be an error + +const animalOrUndef = { type: 'cat', canMeow: true } as Animal | undefined; +assertEqual(animalOrUndef?.type, 'cat' as const); + +animalOrUndef.canMeow; // since is cat, should not be an error