Inverse Type Guards Work In TypeScript
One of the things that I love about TypeScript is that it forces you to think harder about what kind of values you have at your disposal. One of the ways in which TypeScript does this is by forcing you to check the Type of value that you have before your code implicitly downcasts it to a Sub-class. This Type check is known as Type Guard. And, in all the Type Guard examples that I've seen, the condition is a positive assertion. But, the TypeScript compiler is smart enough to understand a negative assertion, and how it affects the flow of control after the guard block.
To be clear, when I say that most Type Guard examples use a positive assertion, I mean that they look something like this:
if ( value instanceof SomeClass ) {
// Inside this block, the TypeScript compiler will treat "value" as an instance
// of the "SomeClass" class, which means it's safe to access "SomeClass" properties.
console.log( value.someClassProp );
}
Here, I'm asserting that the given value is of a given Sub-class type. Which, in turn, allows me to access Sub-class properties safely within the bounds of the given block.
With a negative Type Guard assertion, I can break out of a method if the given value is not of a given type. Then, all of the code after the Type Guard will safely be able to access properties on the given type.
To see what I mean, take a look at this example:
class A {
public aProp: string;
}
class B extends A {
public bProp: string;
}
var a = new A();
a.aProp = "a1";
var b = new B();
b.aProp = "a2";
b.bProp = "b2";
// ----------------------------------------------------------------------------------- //
// ----------------------------------------------------------------------------------- //
inspectValue( a );
inspectValue( b );
function inspectValue( value: A ) : void {
console.log( "A:", value.aProp );
// In this NEGATIVE ASSERTION Type Guard, we're asserting that the value is NOT
// of a given type; and, if so, we're breaking out of the method.
if ( ! ( value instanceof B ) ) {
return;
}
// The negative assertion Type Guard above becomes an implied POSITIVE ASSERTION
// for all code that followed the Type Guard. And, TypeScript is smart enough to
// understand this relationship.
console.log( "B:", value.bProp );
}
In this demo, the first half of the inspectValue() method knows that the given value is of type "A" because that is the type annotation on the parameter. We then have the negative assertion Type Guard which breaks out of the method if the given value is not of type "B". This negative assertion guard condition implies that the latter half of the inspectValue() method can assume that the given value is of type "B" (otherwise, the control flow would have executed the "return" statement). And, the TypeScript compiler is smart enough to understand this.
So, when we run the above code through the ts-node executable, we get the following output:
As you can see, the TypeScript compiler did not complain that we accessed "value.bProp" on a parameter with an "A" type annotation.
TypeScript is awesome. It provides the code with a degree of self-documentation and forces you to think about the relationship of your objects to the consuming context. I'm just surprised the TypeScript compiler is smart enough to understand how type assertions - both positive and negative - impact the assumptions that can be made.
Want to use code from this post? Check out the license.
Reader Comments
Good post. I use this sort of thing with union types quite frequently.
A contrived example
const myFunc = (p1: number | number[]) => {
if (!_.isArray(p1)) {
//here p1 must be number because of type definition for _ being similar to
// function isArray(x: T | T[]) : x is Array<T>
}
}
@Ian,
That's pretty cool that TypeScript understands how to translate lodash's "is" functions into Type Guards. I'll have to look at the Definitely Typed library to see what the signature is. I'm just so impressed with what TS can do. Such a pleasure.
>> "That's pretty cool that TypeScript understands how to translate lodash's "is" functions into Type Guards."
It's called a User Defined Type-Guard in the doc (https://www.typescriptlang.org/docs/handbook/advanced-types.html).
It looks like this. Note the return type is a strange "x is Y" expression.
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
@Mason,
That's really funky :) Hmm, I have to let that sink in.