TypeScript Tricks: Type Guards
matthewgerstman.com
matthewgerstman.com
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...
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.
I would encourage people to consider Reason or Dart or Elm or even plain old ES6 before they decide Typescript solves their problem.
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!
Typescript requires any for ES6 compatibility, but if your code is converted enough ("enough" occurring pretty early in conversion projects in my experience) you can set the compiler flag to forbid non-explicit usage of any.
Dart's type system is worse because you can pass in null for any type at any time. This circumvents any sense of type safety your method signatures might suggest. It's no better than Java.
You are right that that checking for nulls is a bit weird but you just set default values using ?. and. ?? and get on with your life - it doesn't make the whole type system bad and there will be attempts to fix this in the future.
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.[1]: https://github.com/Microsoft/TypeScript/issues/10734#issueco...
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....
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)
https://basarat.gitbooks.io/typescript/docs/types/typeGuard....
Tag encoding can be quite clever. For example, ReasonML's BuckleScript compiler can encode a sum type like this:
type chequeNum = string;
type paymentMethod =
| Cash
| Cheque(chequeNum);
... as the number 0 for the `Cash` case and as a single-element array for the cheque case.See e.g. https://reasonml.github.io/en/try.html?rrjsx=true&reason=C4T...
@rtti
function moo(foo: string): number {
…
}
=> function moo(foo: string): number {
…
}
moo.__type = {
parameters: [{name: "foo", type: "string"}]
returnType: "number"
}
or something like that.Feel free to comment on it, but it seems it's impossible that they implement such kind of thing without breaking their "principles".
Also, the language should proably offer to generate these guards (and the requisite reified type information) for you on demand.
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.[1] https://github.com/tc39/proposal-optional-chaining
var person = {address:{country:"uk"}}
var {address: {country: countryName}} = person var person = {address: null}
var {address: {country: countryName}} = person var [a,b,c] = [1]
Either way, I'd highly recommend just using TypeScript. var [a, b, c] = null
and this (destructing a nested array) var [[a]] = []
the point of the "elvis" ?. operator is to make accessing properties _of_ optional values or other optional properties easier."Either way, I'd highly recommend just using TypeScript."
Sure, I'd recommend that too, but in this case it "only" helps us by moving the error from runtime to compile-time, nested optional property access remains ugly in the TS world, too. The ?. operator will come to TypeScript as soon as it comes to EcmaScript.
const maybePets: Array<Pet | undefined> = [...]
const pets: Array<Pet> = maybePets.filter((v: Pet | undefined): v is Pet => v != undefined)
Will compile without errors.[0]: https://medium.com/@wbobeirne/mutually-exclusive-react-propt...
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.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.
console.log(person.spells); // <-- compilation error
if (!isWizard(person)) {
return[];
}
return person.spells; // <-- this is fineAs to your code, sure, it's an error. But it's not unusual, conceptually. For example:
if (foo) {
int bar = 22;
}
print(bar); // compilation error??
And this: if (foo) {
print(bar); // compilation error??
int bar = 22;
}
More idiomatically, you'd expect to read code like this: if (isWizard(person)) { person.wizardStuff(); }
I think this is not an unusual thing to do in dynamically typed languages, especially when interfacing with third-party code where you can't easily add new polymorphic methods (so you can't rely on virtual dispatch to handle your if-case logic). Making it work more ergonomically in typescript isn't "absolutely bonkers".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.htmlI 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.
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.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.
function getSpells(person) {
if(person.type != "Wizard") throw new Error("person.type=" + person.type + " is not Wizard");
}