
TypeScript Tricks: Type Guards - mattgerstman
https://www.matthewgerstman.com/ts-tricks-type-guards/
======
peterkelly
Discriminated unions [1] are what you're looking for here. Contrary to what
the article claims, the functions shown are _not_ 100% type safe, because if
you define the following:

    
    
        interface NotActuallyWizard extends Person {
            type: PersonType.Wizard;
            otherStuff: string;
        }
    

and then write:

    
    
        const JohnSmith: NotActuallyWizard = {
            type: PersonType.Wizard,
            name: 'John Smith',
            otherStuff: 'whatever',
        };
    
        getSpellsForPerson(JohnSmith);
    

it will pass the type checker (isWizard returns true because it doesn't take
into account the possibility that _another_ interface may have a 'type'
property of PersonType.Wizard) but the code will fail at runtime.

Instead, you should define the interfaces as follows:

    
    
        interface PersonBase {
            name: string;
        }
    
        interface Wizard extends PersonBase {
            type: 'Wizard';
            spells: string[];
        }
    
        interface Muggle extends PersonBase {
            type: 'Muggle';
            dursley: boolean;
        }
    
        type Person = Wizard | Muggle;
    

then replace the type guards with:

    
    
        if (person.type !== 'Wizard') {
    

and

    
    
        if (person.type !== 'Muggle') {
    

This will report an error for getSpellsForPerson(JohnSmith) because the
compiler knows that (1) the only possibilities for Person are Wizard and
Muggle, (2) both have type properties (3) the type properties have distinct
literal values, and therefore (4) a Person with a type property of 'Wizard'
has must have a spells property and a Person with a type property of 'Muggle'
must have a dursley property.

As explained in [1], you can also take advantage of these with switch
statements, and the compiler will do an exhaustiveness check so you catch any
cases where you add a new type but forget to update one of your switches.

[1]
[https://basarat.gitbooks.io/typescript/docs/types/discrimina...](https://basarat.gitbooks.io/typescript/docs/types/discriminated-
unions.html)

~~~
fingerlocks
The problem is not the use of type guards, but rather the author's
implementation of them. The entire point of type guards is to narrow a
variable's type within a block- e.g. to avoid accessing undefined members.

If we rewrite isWizard() to check for the existence of the `spells` variable,
your counterexample will fail the type guard and JohnSmith will be exposed as
the NotActuallyWizard he really is.

~~~
andy_ppp
Almost as if Typescript is so complicated that getting your types "correct" is
nearly impossible, especially when you add in the high variability in the
quality of provided types on DefinitelyTyped [1].

[1]
[https://github.com/DefinitelyTyped/DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped)

~~~
moomin
I don't really agree with this take. The point is that TypeScript allows you
to type standard JavaScript idioms. The whole "type equals string" thing is a
standard JavaScript idiom. Are discriminated unions better? Yes. But we are
where we are and JavaScript is complicated.

~~~
andy_ppp
There are lots of better languages that compile to JS and don’t let you make
mistakes in the same way the two grand parents do. Just saying JS is
complicated to make typed is completely true and not a good reason to use TS;
it doesn’t excuse the fact that I spend all this time doing a Typescript dance
and still wonder if my Types have weird flaws.

I would encourage people to consider Reason or Dart or Elm or even plain old
ES6 before they decide Typescript solves their problem.

~~~
vga805
If anyone gives these other compile-to-js languages a look, I would add
ClojureScript to that list, especially if doing React work. The re-frame
framework is great, and the cljs ecosystem is a joy imo. You won't get the
kind of type system these other langs provide, but there's some interesting
stuff going on with spec.

~~~
andy_ppp
Agree strongly ClojureScript - shame it has a terrible name which probably
means it won't gain the popularity it deserves. I've also heard good things
about PureScript.

~~~
vga805
I haven't been able to put my finger on why cljs isn't more popular,
especially given all of the great stuff David Nolen did in the early days of
React and the ease of use of figwheel and some other cool things in the
ecosystem.

I always just assumed it was too big a syntactical hurdle for lots of teams.
I.e., I suspect a lot of individuals appreciate it, but it's hard to choose
cljs from the business side of things because it looks so foreign to many.
That's a shame!

Haven't looked into PureScript yet. I'll add it to the list!

------
city41
Be careful though, as they are not type safe in the sense the compiler
completely trusts the type guard is implemented correctly.

In other words, TypeScript has no problem with this:

    
    
        function isNumber(a: any): a is number {
            return typeof a === 'string';
        }
    

Type guards are usually simple functions, but I've gotten them wrong before
and only found out with a runtime exception.

~~~
benschulz
Yes, I really wish that simple arrow functions were inferred for that reason.
I've implemented a proof-of-concept[1], but I don't see it being added to the
compiler any time soon. It's a shame because it would make a lot of my code
more robust.

[1]:
[https://github.com/Microsoft/TypeScript/issues/10734#issueco...](https://github.com/Microsoft/TypeScript/issues/10734#issuecomment-351570480)

------
jasonkillian
Type guards are a useful construct, especially when dealing with data from an
API that could come in a variety of shapes.

However, if you have control over the data shape, in general, it's nicer to
take advantage of discriminated unions as you'll get more automatic typesafe
compiler support. If you're not familiar with the construct in TypeScript, the
docs give a nice example of them[0].

[0]: [https://www.typescriptlang.org/docs/handbook/advanced-
types....](https://www.typescriptlang.org/docs/handbook/advanced-
types.html#discriminated-unions)

------
eranation
Just to clarify when this is useful - for example, it's great when you receive
a typeless JSON object from the outside world, and want to assert its "type"
based on structural typing.

If it's not an external object (e.g. created in TypeScript with new), you can
just you instanceof / typeof of course.

To be more precise, instanceof, typeof etc are also sort of automatic type
guards in TypeScript, and the example in the post is more of a manual type
guard (e.g. if you get something from the outside world, without type
information, and want to rely on its structure to assert its type)

More on this:

[https://basarat.gitbooks.io/typescript/docs/types/typeGuard....](https://basarat.gitbooks.io/typescript/docs/types/typeGuard.html)

------
_hardwaregeek
While I understand the purpose of TypeScript's type erasure, I kinda wish
there was an opt-in type reification so that type guards could be
automatically created. I don't really like how they rely on the user to get
runtime type assertions correct. But I guess then there'd have to be a TS
runtime of sorts.

~~~
skrebbel
Agree. It would be awesome if TypeScript had some optional run-type
information, maybe in the form of a decorator function

    
    
        @rtti
        function moo(foo: string): number {
            …
        }
    

=>

    
    
        function moo(foo: string): number {
            …
        }
        moo.__type = {
            parameters: [{name: "foo", type: "string"}]
            returnType: "number"
        }
    

or something like that.

~~~
stupidcar
It does: [http://blog.wolksoftware.com/decorators-metadata-
reflection-...](http://blog.wolksoftware.com/decorators-metadata-reflection-
in-typescript-from-novice-to-expert-part-4)

~~~
TomMarius
It's very basic, though.

------
norswap
I get why this kind of things would be necessary (e.g. calling TypeScript from
JS), but man, if it isn't ugly and confusing.

Also, the language should proably offer to generate these guards (and the
requisite reified type information) for you on demand.

------
miguelrochefort
The two things miss in TypeScript coming from C#:

Pattern matching:

    
    
        if (thing is Person person)
        {
            var name = person.Name
        }
    

Safe navigation operator:

    
    
        var countryName = person.Address?.Country ?? "Unknown";
    

Type guards are a pain to deal with.

~~~
patates
Lack of the safe navigation operator is a real PITA. You can replace "??" with
"||" (more or less), but no solution exists for "?." and I don't know how I
lived without it before.

~~~
vimslayer
Luckily, we'll probably get it sooner or later [1]. There are also a bunch of
typesafe getter function utility libraries that are better than nothing [2-3].

[1] [https://github.com/tc39/proposal-optional-
chaining](https://github.com/tc39/proposal-optional-chaining)

[2] [https://github.com/pimterry/typesafe-
get](https://github.com/pimterry/typesafe-get)

[3] [https://github.com/yayoc/optional-
chain](https://github.com/yayoc/optional-chain)

------
drabinowitz
Type guards also allow you to use Array.prototype.filter to modify the type of
an array ie

    
    
      const maybePets: Array<Pet | undefined> = [...]
      const pets: Array<Pet> = maybePets.filter((v: Pet | undefined): v is Pet => v != undefined)
    

Will compile without errors.

------
wbobeirne
I wrote a very similar article about how to deal with having different sets of
prop types this way[0]. I was pretty disappointed that TypeScript wasn't able
to infer a lot of this information, and that it doesn't let you access props
from one of the possible types if it's not available on others in the union.

[0]: [https://medium.com/@wbobeirne/mutually-exclusive-react-
propt...](https://medium.com/@wbobeirne/mutually-exclusive-react-proptypes-
with-typescript-97cfd2ebc6a0)

------
muglug
I created something similar in my PHP typechecker, powered by annotations:
[https://getpsalm.org/r/d882a4bb45](https://getpsalm.org/r/d882a4bb45)

------
codemusings
Wait a second. How does the following even compile:

    
    
        function getSpellsForPerson(person: Person) {
          if (!isWizard(person)) {
            return [];
          }
          return person.spells;
        }
    

You can't return `person.spells` without type casting:

    
    
        return (<Wizard>person).spells;
    

or by accessing the field with array syntax:

    
    
        return person["spells"];
    

The latter one obviously discards type safety.

~~~
barrkel
It's because the compiler uses the peculiar return type annotation on
isWizard() to infer information about the argument and propagate it along one
half of the control flow graph based on the result of isWizard().

It sees that the code `person.spells` is guarded by a truthful return from
isWizard, and it trusts the annotation that person is Wizard as a result.

~~~
codemusings
That's absolutely bonkers. What's the use case here? Also does that mean the
following:

    
    
      console.log(person.spells); // <-- compilation error
      if (!isWizard(person)) {
        return[];
      }
      return person.spells; // <-- this is fine

~~~
adrusi
Typescripts goal is to allow using existing JavaScript idioms while gaining
the advantages of static type checking.

I don't think it's too bonkers for the type of a variable to change within the
same scope, as long as there's some clear delineation. In rust you can
redeclare a variable with the same name but different type:

    
    
        let x: i32 = 99;
        println!("{}", x);
        let x: &str = "shadows";
        println!("{}", x);
    

Kotlin does something very similar to typescript for null checks:
[https://kotlinlang.org/docs/reference/null-
safety.html](https://kotlinlang.org/docs/reference/null-safety.html)

I don't know what's so crazy about a variable's type changing within the same
scope except that languages from before 2010 mostly didn't allow that.

~~~
steveklabnik
To be clear, this doesn’t change the type in Rust. It creates a new variable
with the same name. Very different thing!

------
zapzupnz
Having to write these manually seems a bit … boilerplate-y. Why do we need the
PersonType enum rather than just depend on the peoples' types? Why do we need
functions to get spells for a person; why not just access the property
directly on wizards? Why would you ever want to return an empty array of
spells for a muggle rather than just return nothing?

Was it all just to illustrate the point? Because illustrating with
unnecessarily complicated code is … unnecessarily complicated, I'd have
thought.

I don't know TS well enough to rewrite this, but here's my attempt is Swift at
the sample code, but cleaner. I'm sure this is all possible in TS, based on my
few usages of it:

In Swift:

    
    
      protocol Person {
        let name: String
      }
    
      protocol Muggle: Person {
        let isDursley: Bool
      }
    
      protocol Wizard: Person {
        let spells: [Spell]
      }
    
      protocol Slytherin: Wizard {
        let darkArts: [DarkArt]
      }
    
      protocol Ravenclaw: Wizard {
        let extraKnowledge: [KnowledgeItem]
      }
    
      protocol Hufflepuff: Wizard {
        let badgerFriends: [Badger]
      }
    
      protocol Gryffindor: Wizard {
        let loyalTo: [PersonName]
      }
    
      let people = [
        Gryffindor(name: "Hermione Granger", spells: ["Petrificus Totalus"], loyalTo: ["Ron Weasley"]),
        Gryffindor(name: "Ron Weasley", spells: ["Wingardium Leviosa"], loyalTo: ["Hermione Granger", "Harry Potter"]),
        Gryffindor(name: "Harry Potter", spells: ["Expelliarmus", "Expecto Patronum", "Imperio"], loyalTo: ["Albus Dumbledore"]),
        Slytherin(name: "Draco Malfoy", spells: ["Serpensortia"], darkArts: ["Cruciatus", "Avada Kedavra"]),
        Hufflepuff(name: "Cedric Diggory", spells: ["Transfiguration"], badgerFriends: ["Wayne"]),
        Ravenclaw(name: "Luna Lovegood", spells: ["Expecto Patronum"], extraKnowledge: []),
        Muggle(name: "Dudley Dursley", isDursley: true),
        Muggle(name: "Prime Minister", isDursley: false)
      ]
    
      let slytherins = people.filter { $0 is Slytherin } // count is 1
      let ravenclaws = people.filter { $0 is Ravenclaw } // count is 1
      let hufflepuff = people.filter { $0 is Hufflepuff } // count is 1
      let gryffindors = people.filter { $0 is Gryffindor } // count is 3
    
      let muggleNames = people.filter { $0 is Muggle }.map { $0.name }
    
      let extraInformation: [[Any]] = people.map { person in
        if let slytherin = person as? Slytherin {
          return slytherin.darkArts
        } else if let ravenclaw = person as? Ravenclaw {
          return ravenclaw.extraKnowledge
        } else if let hufflepuff = person as? Hufflepuff {
          return hufflepuff.badgerFriends
        } else if let gryffindor = person as? Gryffindor {
          return gryffindor.loyalTo
        } else if let muggle = person as? Muggle {
          return muggle.isDursley
        }
      }
      

Didn't have to write any of those accessors, mutators, or futz around with any
more type checking than absolutely necessary. I'm sure basically the same is
possible in TypeScript, too.

~~~
nothrabannosir
Because of type erasure. The compiler removes all type information, and these
checks are actually to support a common _JavaScript_ paradigm (doing manual,
run time checks).

Often found in code checking function argument types to determine which
version of a “polymorphic” function was called, e.g. jQuery’s $.post.

TypeScript is all about annotating existing JavaScript code, so they strive to
support the full JavaScript vernacular.

[http://api.jquery.com/jQuery.post/](http://api.jquery.com/jQuery.post/)

~~~
zapzupnz
Ah, so TS types are basically only available at compile time? Does that make
the "standard" way to do things to have both TypeScript types AND some
alternative form of type information in object prototypes/classes for runtime
use? That seems a bit redundant to my ignorant eyes, complicating what was
already a complicated dance.

~~~
patates
You cannot deal with Javascript without a "complicated dance". I like JS, and
TS even more, but facts are facts.

~~~
z3t4
The intermediate programmer likes to write smart and complicated code, but
after a while you will go back to basics, and start to appreciate simplicity
and convenience, and write code similar to a beginner, but without the bugs
and inefficiencies (including your own time).

~~~
patates
I didn't mean it was to be liked. I meant that I like it in spite of the
unnecessary complexity.

------
z3t4
Vanilla JavaScript:

    
    
        function getSpells(person) {
            if(person.type != "Wizard") throw new Error("person.type=" + person.type + " is not Wizard");
        }

