Since this has caught me out a number of time recently, I decided it's time to add another resource to any future search results :)
/**
* Let's say you're declaring the 'shape' of messages
* that your application can accept in a union like so...
*/
type Msg =
| { kind: "sign-in"; email: string; password: string }
| { kind: "sign-out" };
Type 'Assertions'
The dangerous kind. Typescript allows the following snippet and does not warn us that the email
and password
fields are absent.
It's very easy to imagine the bugs introduced later when handling a sign-in
message and expecting email
and password
to exist.
/**
* Ooops! These `As Expressions` force Typescript to believe *us* that
* `msg1` & `msg2` are members of the `Msg` union above.
*/
const msg1 = { kind: "sign-in" } as Msg;
const msg2 = <Msg>{ kind: "sign-in" };
So, even though { kind: "sign-in" }
is NOT actually a valid Msg
- it only could be if the email
& password
were also present - that doesn't stop Typescript from allowing it in any future place that accepts Msg
.
This means that when you attempt to narrow the Msg
union later - such as in a switch
statement - you could run into errors when you expect email
and password
to actually exist.
Use with caution, and only when you think you know more than
Typescript (see: hardly ever đŸ¤£)
Type 'Declarations'
The safer way. Use this instead when you want Typescript to actually error when the element in question does not satify the type exactly.
/**
* Better! Typescript will warn us here that `msg3` is invalid.
* Adding the `email` and `password` fields would remove the error đŸ˜€
*/
const msg3: Msg = { kind: "sign-in" }; // Error!
Top comments (4)
Thanks for this very nice example! I understand the difference for complex types (objects, unions etc.)
Is there a difference of a type assertion and a type declaration in the following three cases?
string
, for example iftype Msg = string
?string[]
, for example iftype Msg = string[]
?any
, i.econst msg: any = ...
andconst msg = ... as any
?If there is no difference there is no danger in using a type assertion in these cases, right?
P.S. You use the term "type declaration" - you're referring to a "type annotation", right?
I do not like the ability to define different types for the same object. It is, to me, already in place with type any. If we insist on different types they should be contained not inherited in my opinion. So in this case Msg could contain either a login or logout type. That way there's never any potential problem and it carries almost no extra overhead.
There's no 'inheritance' involved in this approach, Typescript has good support for tagged/discriminated unions - it's only the addition of 'as' here that causes problems.
This style is often used by devs familiar with similar things in other languages, like in Rust, it would be
You're right, I used the term inheritance; incorrectly to describe a pattern based on the single strong type. It is a type at layer zero, which could be either inherited or contained.
If we define something to be more than one type, and we don't use a containment pattern then, we are propelled to use the typescript multi-type system, which you show here.
When we use a field within an object to describe the type, we still achieve polymorphism, but we have the potential to run into issues as you describe here. I've seen this mistake happen over in older JavaScript code, where the freedom of creating objects, anywhere, anytime, anyplace causes issues because of missed properties, property key's being misspelled in some of the objects, expecting functions that aren't there; and the issues with not following a type system. This is why I prefer strong types and the containment pattern like this:
The problem of accidental type confusion is resolved because both logIn and logOut are typed. Any attempt to sidestep the type constraint is immediately caught at code time. But here, we would need a discriminator as we have both types contained and we are easily able to determine their state.
I see the 'as' operator to be a casting mechanism. When it's used with non-strongly typed code, then the issue you describe is more than just a typescript error, it's how JavaScript has been operating with the loosely typed system for 20 years now.
I did gain more insight from your article on the newer polymorpic behaviors that Typescript is now support. Thanks I'll look into it more.