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;
}
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:
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.
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.
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].
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.
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.
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.
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.
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!
In ES6, every variable is "any" and implicitly nullable.
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.
I agree with you but we were talking about Dart vs Typescript. ES6 is fine if you’re sketching something and wanting a flexible (i.e. subject to change) view of something, if you have a clear/certain design there are far better systems than building the most complicated type system that both the OP and the GP can’t tell are truly type safe? Why on earth would you choose to build your type safe system in something this broken? If you don’t understand why the other systems are better that’s fine but I think try doing Golang, then Lisp, then Haskell, then maybe Elm or even Dart you’ll see that Typescript is just a lie that causes huge amounts of extra work on top of your usual work. Just my opinion but the effort/reward/beauty ratio is all wrong in TS.
I don't think you understand Dart's type system. It has “dynamic”, which is equivalent to “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.
I have used Dart's type system and prefer it to Typescript because it's much simpler and better integrated into the language. I think "dynamic" is better than "any" because the type is inferred by dart which gives more type safety than Typescript does here. Obviously TS won't have a clue what to do after an "any" is passed into another Type afaik?
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.
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.
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].
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)
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.
Not 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.
io-ts[1] is a great solution for implementing runtime-types. The most interesting feature is that the static type can be "extracted" out of the runtime type so in most cases there is no duplication of information between runtime type and static type.
One 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.
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.
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].
That 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
Right, because while b and c will be undefined, they are taken from an existing array, not from null/undefined.
This will throw
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.
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.
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.
The 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".
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);
The 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 personis a wizard.
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.
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.
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.
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).
I think it is. But I also think a significant amount of that awkwardness comes from the DOM and all the backward-compatible cruft that comes with it. I'm ready to be positively surprised but I'm not sure how much of those stuff you can abstract away.
Instead, you should define the interfaces as follows:
then replace the type guards with: and 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...