10 ms·
TypeScript Tricks: Type Guards
- city41 8y agoBe 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 8y agoYes, 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#issuecomment-351570480 https://github.com/Microsoft/TypeScript/issues/10734#issueco...
- eranation 8y agoJust 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.html https://basarat.gitbooks.io/typescript/docs/types/typeGuard....
- jasonkillian 8y agoType 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.html#discriminated-unions https://www.typescriptlang.org/docs/handbook/advanced-types....
- _hardwaregeek 8y agoWhile 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.
- recursive 8y agoOne of the guiding principles of Typescript is that it compiles to javascript. There isn't really a generally correct and practical way to do arbitrary type checking at runtime. For many types there is, such as using `instanceof` or `typeof`, but those don't work for testing interface compliance. Also, any type with a generic parameter is going to be hit-or-miss.
- seanmcdirmid 8y agoNot just compiles to JavaScript, it compiles simply via type erasure and lacks a runtime.
- deleted 8y ago[deleted]
- yawaramin 8y agoNot necessarily. ML-family languages which compile to JS, like ReasonML, can encode sum types in a variety of ways. (Sum types are essentially type reification because the alternative cases are 'tagged' with names). 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=C4TwDgpgBAxgFhAjgVwgOWQWygXigZ2ACcBLAOwHMBuAKFEijAEMRMIzgBZCYOAewAmuGgB8oAYSb44oiQhQQAFPCSoMmAJS0aAGx5RgfAMrFyFXFABmyMjShQxk6bgB8UAEQAFJiSHlYUnDudg5yqkoqCuoarlAA3gBWIt6+UADuJLyw8qhQZHwAdFAAJJFqWCIJAL7aesAB0t6s7PV4TnC0ddnhTWwcFuI5Su4AjABMAMwALO5aNEA https://reasonml.github.io/en/try.html?rrjsx=true&reason=C4T...
- 8y ago
- wbobeirne 8y agoI 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-proptypes-with-typescript-97cfd2ebc6a0 https://medium.com/@wbobeirne/mutually-exclusive-react-propt...
- peterkelly 8y agoDiscriminated 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/discriminated-unions.html https://basarat.gitbooks.io/typescript/docs/types/discrimina...
- fingerlocks 8y agoThe 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 8y ago
- muglug 8y agoI created something similar in my PHP typechecker, powered by annotations: https://getpsalm.org/r/d882a4bb45 https://getpsalm.org/r/d882a4bb45
- z3t4 8y agoVanilla JavaScript: function getSpells(person) { if(person.type != "Wizard") throw new Error("person.type=" + person.type + " is not Wizard"); }
- zapzupnz 8y agoHaving 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.
- miguelrochefort 8y agoThe 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 8y agoLack 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 8y agoLuckily, 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
- seanwilson 8y agoMaybe I'm missing something but can't you do this for the second one where countryName will be undefined if it's not set? var person = {address:{country:"uk"}} var {address: {country: countryName}} = person
- vimslayer 8y agoThat would work, but what if the `Person` type's address can also be null? The following would be an error either at runtime or - in the case of TypeScript - at compile time. var person = {address: null} var {address: {country: countryName}} = person
- seanwilson 8y ago
- codemusings 8y agoWait 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.
- deleted 8y ago[deleted]
- __ryan__ 8y agoThe type guard returns true if person is a wizard. The if statement handles the case that person is not a wizard. The only case left is that person is a wizard.
- barrkel 8y agoIt'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 8y agoThat'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
- deleted 8y ago[deleted]
- barrkel 8y agoThe use case is having types depend on control flow so that guarded control flow paths don't need further typecasting. I don't see how this is bonkers. It looks like a great ergonomic feature to me. As 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".
- drabinowitz 8y agoType 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.
- norswap 8y agoI 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.