06 August 2019

Type checked "visitor" for discriminated unions using mapped types

Discriminated unions are one of the most useful features of TypeScript. After testing the discriminator value TypeScript can apply type checking based on the relevant member type of the union.

So, for a discriminated union of shapes, like this...

It's possible to write a calculateArea function using a switch on the discriminator (__typename in this case), like this...

Note that you can use the correct properties for the relevant member type of the union in each case body and you'll also get Intellisense prompting you with the correct property names.

Exhaustiveness problem

However, depending on the TypeScript compiler options you have set, you may end up missing cases where a type not covered by a case in the switch just falls through and the function returns an undefined. Adding the --noImplicitReturns options causes the compiler to complain if this is the case but this may not be an option for you if you make use of implicit returns. Another work around is to assign your instance to never in the switch's default e.g.

This will force the compiler warning but that is something you always need to remember to add when using a switch like this so it's not exactly a "pit of success".

Mapped types method

There is an alternative to the switch using mapped types instead which allows you to declare a map of the discriminator value to the correct operation for the type, like this...

The UnionMap mapped type which makes this work is defined like this...

It makes use of conditional types to enumerate the member types of the union in order to collate the discriminator values into a type e.g. UnionKeys<Shape, '__typename'> will be 'Circle' | 'Square' | 'Rectangle' | 'Triangle'. The mapped type UnionMap then requires a key for each of these string values to be present using [K in ...]: and it then does a "reverse lookup" of the original member type for that key using UnionPartForKey in order to provide the function argument type.

Using this method is cleaner and less error prone than the switch method. It will cause compile errors even with relaxed compiler options if a discriminator value is missed out of the map object.

Playground

I've created a TypeScript playground with mapped types here.

References

Written with StackEdit.

24 July 2019

TypeScript Gotchas: Type Assertions

TL;DR

In summary, unless you want lots of weird non-checking of your types avoid mixing type assertions and literals. The critical thing to remember is that when you use a type assertion you're telling TypeScript that you know what the type is and it doesn't need to check. If there is genuinely no way TypeScript can check the value, i.e. it's a runtime value; maybe it came back from a web service, then it's valid to use the assertion (possibly after you do your own validation). But if you're writing a literal value in TypeScript then the compiler knows everything about the value and the context so you shouldn't assert anything and should leave the compiler to do its job in peace.

The details...

I think TypeScript is the best thing to happen to JavaScript since Douglas Crockford's JavaScript: The Good Parts and the linters which checked your code against its recommendations. However in many ways TypeScript is much more a security blanket than a safety net as it's easy to break its type checking without realising. I'm going to dive in to a few of the most common gotchas (in my limited experience using it) in a couple of blog posts. This first post is reserved for, what I consider the worst offender, declaring literals using type assertions.

There is only a difference of one character in length between these two statements but the behaviour in terms of type checking is very different. It is very common to see this as shorthand in places where you don't normally use an explicit declaration such as a return value of a lambda e.g.

When you start writing literals in this way in one place in your code, you'll fall into a habit of using this everywhere.

I think one of the main reasons why this is so prevalent is that the behaviour of the IDE when using a type assertion like this is very similar to declaring a value the "right" way. You get intellisense prompting you with the property names of the asserted type...

...and when you start typing, if you provide an incorrect property name, you get a compiler warning...

These things are all pointing to the type checking on this value being no different to that on a standard declaration - it lulls you into a false sense of security.

A note on version - this is true as of version 3.5.1.

I created a TypeScript playground with these examples in for you to follow along.

Object literals

In the example above, rather than defining a literal of a target (asserted) type what you're actually doing here is defining a literal of an implicit type which you then assert as the target type so it's the same as doing this...

For example, taking this trivial type with two required properties and one optional...

When processing the type assertion TypeScript simply checks that any properties on the source type with a name that matches one of the target type's properties also have the same type as that property. The first gotcha about this is that, if there are no properties on the source type, it matches even if the target type has required properties...

If properties are declared on the source you need to supply all the required properties of the target but after that any additional properties that may be present on the source type are disregarded unless they are a name match with a target property. The gotcha arising here being that, if you have a typo in the name of an optional property, you won't know about it.

In `foo5` and `foo6` the property `other` is incorrectly spelled `othen` but only the literal without the assertion catches this.

Primitives

For primitives type assertions error as you would expect for assigning a value of one type to a variable of a different type albeit with a much more long-winded error message.

Things are a bit more interesting with union types of constant values, though. For example, with a union of three constant string values the assertion allows an incorrect string value but it will fail if given a value of another underlying type e.g. a number.

Arrays

Arrays and type assertions don't agree at all. The assertion doesn't catch any wrong values added to the array...

Tuples

Tuples, however, are a different story and the assertion is a lot more strict with it essentially behaving the same as a standard declaration.

Summary

As mentioned in the TL;DR - don't mix literals with type assertions. It is fraught with type checking peril and the use of assertions should be restricted to only the cases where TypeScript genuinely has no information about a value.

For the shorthanded lambda return values just ensure you define the return type on the function type itself e.g.

For covariant return types writing the declarations out longhand is the safest option.

Up next time - why you shouldn't use parameters with your functions!

27 June 2019

Date timezone changes in Chrome 67

Beginning in Chrome 67 (released on 29 May 2018) there was a change to how timezones are handled in the JavaScript Date object. Historical dates will now have a historically accurate timezone offset applied to them which means that, if you were supplying a UTC date/time to one of the new Date() overloads and then retrieving local time, the value you get back may have changed from Chrome 66 to Chrome 67.

For example, with the machine timezone set to London, if you evaluate...

new Date(0).getHours()
  • 0 ... on Chrome 66
  • 1 ... on Chrome 67

0 here is treated as a milliseconds offset from Unix epoch time (1970-01-01T00:00:00Z) so the date is holding a value of 1970-01-01T00:00:00Z.

getHours returns a local timezone adjusted version of that value which, by today’s timezone offset rules, is still ‘0’ because on 1st Jan we’re at UTC+0 and the historical time adjusted by today's offset rules is what Chrome 66 gives us.

Chrome 67 applies the correct historical offset that was in effect on that date for the current system timezone. Weirdly in 1969 and 1970 London didn't observe a daylight saving change and was at UTC+1 for the whole year hence the value returned by getHours() is `1` because the local time was 1970-01-01T01:00:00+01:00.

A common use of the new Date(milliseconds) constructor overload method of creating a Date object is using a small number of milliseconds to format a duration e.g. "01:30:38 remaining" discarding the date part altogether and a similar problem to this was highlighted by Rik Driever in his post on the change [1].

It was Rik's post that led me to the Chromium change which introduced this new behaviour [2] and the related change to the ECMA standard [3].

In Rik's post he concludes that his issue is down to incompatibility between JavaScript and the .NET's JavaScriptSerializer and he attempts various workarounds to try and account for the offset being applied to the Date object without much success.

In fact, JavaScript and .NET are working together fine, and there are two easy ways to get your intended value back out of the Date.

Option 1 - Use the getUTC* methods instead

The millisecond value we're passing in is UTC and what we really expect to get out is also UTC so we should use the getUTC* methods instead e.g. getUTCHours(), getUTCMinutes(). The fact that getHours() was returning the value we expected in Chrome 66 and before was a coincidence and we should never have been using getHours() in the first place.

Some code coincidentally giving you the right value so you assume it's correct is a very common cause of bugs and this is a great example. It's also a great example of why you should use constants as the expected values in unit tests because if you were to use a value returned by new Date(blah).getHours() as your expected value your test would still pass.

Option 2 - Initialise the Date with a local time

If you want to keep using the local offset methods of the Date e.g. getHours(), getMinutes() then you can initialize the date slightly differently to get the result you expect:


new Date(1970, 0, 1, 0, 0, 0, milliseconds)

This overload of new Date() expects a local time so doing this instead will initialise the date to midnight local time and the constructor gracefully handles a millisecond value greater than 999 by just incrementing the other parts of the date by the correct amount. So in this case the date being held is 1970-01-01T00:00:00+-<offset> and getHours() will return 0 for any system timezone and any Chrome version.

[1] Rik Driever's post
https://medium.com/@rikdriever/javascript-date-issue-since-chrome-67-50aa555799d0
[2] Implement a new spec for timezone offset calculation
https://chromium-review.googlesource.com/c/v8/v8/+/572148
[3] The ECMA spec change
https://github.com/tc39/ecma262/pull/778