Showing posts with label TypeScript. Show all posts
Showing posts with label TypeScript. Show all posts

Wednesday, 28 March 2018

Making Immutable.JS objects easier to work with in TypeScript: TypeScript 2.8 edition

TypeScript 2.8 was released today and one of the new touted features is conditional types.

With the introduction of condition types, it was worth revisiting an older post of mine about making immutable.js easier to use in TypeScript and see how one would solve this problem with TypeScript 2.8.

For reference, consider these interfaces:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface IGeographicCoordinate {
    lat: number;
    lng: number;
}

interface IPlacemark {
    id: number;
    coordinate: IGeographicCoordinate;
    name: string;
}

At the time of that post (this was before the revolutionary TypeScript 2.0 release), this was the best I could do to work with the immutable.js versions objects that adhered to the shape of the above interfaces.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type GeographicCoordinateProperties = "lat" | "lng";
type PlacemarkProperties = "id" | "coordinate" | "name";

interface IGeographicCoordinateImmutable {
    get(key: GeographicCoordinateProperties): any;
    get(key: "lat"): number;
    get(key: "lng"): number;
}

interface IPlacemarkImmutable {
    get(key: PlacemarkProperties): any;
    get(key: "id"): number;
    get(key: "coordinate"): IGeographicCoordinateImmutable;
    get(key: "name"): string;
}

Notice we had to manually write "immutable" equivalents of every interface and we didn't have constructs like keyof to auto-deduce all the allowed property names. We also had to manually spell out all the specific return types for each property name due to mapped types not existing yet at that point in time.

With TypeScript 2.8, we can leverage conditional types and features from earlier versions of TypeScript to create this majestic piece of generic and type-safe beauty:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface ImmutableObject<T> {
    get<P extends keyof T>(key: P): T[P] extends Array<infer U> ? ImmutableList<U> : T[P] extends object ? ImmutableObject<T[P]> : T[P]; 
}

interface ImmutableList<T> {
    count(): number;
    get(index: number): T extends object ? ImmutableObject<T> : T;
}

// The immutable.js fromJS() API
declare function fromJS<T>(obj: T): ImmutableObject<T>;

So how does this work? To illustrate, lets update our example interfaces to include a 3rd interface


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
interface IGeographicCoordinate {
    lat: number;
    lng: number;
}

interface IPlacemark {
    id: number;
    coordinate: IGeographicCoordinate;
    name: string;
}

interface ISearchResult {
    query: string;
    results: IPlacemark[];
}

Then let's start with the get method of ImmutableObject<T>

The fragment P extends keyof T describes the type placeholder P on this method that is any variable that is a valid member name of type T. Using our above interfaces as an example, keyof IPlacemark is the type equivalent of:

 "id" | "coordinate" | "name"

The type of key parameter of the get method is constrained to any of the member names in T (auto-deduced via the keyof operator), thus you cannot plug in names of members that are not part of T.

The return type T[P] is a mapped type that gives the corresponding type based on the value you put in for the key parameter. If we use IPlacemark as an example:
  • Calling get with "id" will deduce T[P] to the type: number
  • Calling get with "coordinate" will deduce T[P] to the type: IGeographicCoordinate
  • Calling get with "name" will deduce T[P] to the type: string
We then leverage the new conditional types feature to conditionally deduce the appropriate return type based on properties of T[P] that we can ask of through the conditional types feature:
  • If the mapped type is an array (T[P] extends Array) resolve the return type to ImmutableList of the inferred type U. The infer U fragment defines an ad-hoc type placeholder U that will resolve to the item type of the array.
  • Otherwise, if it is an object (T[P] extends object) resolve the return type to ImmutableObject of the mapped type
  • Otherwise, it will resolve the return type to the mapped type.
To illustrate with ISearchResult as an example:
  • Calling get with "query" will deduce a return type of: string
  • Calling get with "results" will deduce a return type of: ImmutableList<IPlacemark>
Now on to the get method of ImmutableList<T>

ImmutableList<T> is a immutable collection wrapper. The get method here simply returns the item of type T at the specified index. Once again we leverage conditional types to ask some questions of the type T to deduce the correct return type.
  • If T is an object (T extends object), resolve the return type to: ImmutableObject<T>, making this mapped type fully recursive all the way down however many levels it needs to go
  • Otherwise T must only be a primitive type, so resolve the return type as-is.
Now how do we know this actually works and is not some abstract piece of theory?

See for yourself on the TypeScript playground (NOTE: If the code fragment doesn't load in the playground, you can copy/paste the code from this gist I've prepared earlier)

No red squiggles! Also, put your mouse over every variable, you will see the type matches the type specified in the respective end-of-line comment.

Hopefully this post has shed some light on how powerful this new conditional types feature of TypeScript 2.8 really is

Friday, 16 March 2018

A TypeScript-powered moment of clarity

Today was a case where I forgot about one of the major strengths of TypeScript: The type-safety that it provides you and the features available to allow for writing code with maximum type-safety.

Consider this simple utility function in TypeScript

1
2
3
export const strIsNullOrEmpty(str: string | null | undefined): boolean {
    return str == null || str == "";
}

The function does what it says on the tin: Checks if the given string is null (or undefined) or an empty string and returns true if that's the case or false otherwise.

There's no problems with using this function, until you turn on null checks or use this function in a TS code-base with null checks enabled.


And if I hover over the red squiggly, I see this:


But how is that possible? I know that by calling this function, there's no way that the variable str could be null or undefined.

This is indeed the case ... at runtime.

At compile-time TypeScript does not know that. So short of spamming exclamation points all over your code, how can we tell TypeScript that the string we passed in cannot be null or undefined if we just checked for it?

The key is to change the function from the above example into this:

1
2
3
export function strIsNullOrEmpty(str: string | null | undefined): str is null | undefined {
    return str == null || str == "";
}

And the red squiggle goes away.

This is feature of TypeScript known as type guards. It's a way to write functions that evaluate to true/false at runtime, but at compile time allows you to make some assertions about the type of the variable you passed in to the TypeScript compiler. In our case, passing in a variable that could be a string or null or undefined has to be either null or undefined if this function returns true. If it returns false, then it has to be a non-null string, which TypeScript knows then to strip of the null | undefined on any subsequent if block following that function call. This is what's known in TypeScript as type narrowing.

Type guards have been in TypeScript for a long time, but it took today for me to realise that some of my utility functions (like this example) should've been written as type guards, especially with the advent of TypeScript 2.0 and its powerful null checking capabilities.

Now to double check places in my code where I've been sprinkling ! operators all over the place.

Thursday, 14 April 2016

Making Immutable.JS objects easier to work with in TypeScript

UPDATE 4/4/2018: Want to see how this is done in TypeScript 2.8? Click here.

I've been playing with React.JS a lot lately (personally and at work), in combination with TypeScript, Redux and Immutable.JS.

In my adventures with this particular front-end stack (starting from this excellent React/TypeScript/Redux starter template), I've been finding the use of Immutable.JS greatly helps in reasoning about code and how state changes flow through various React components.

One of the problems however, is that the Immutable.JS APIs are very dictionary-like (ie. Magic strings galore!) when dealing with immutable objects. The TypeScript definition for this library is also somewhat restrictive with regards to immutable objects, insisting that your immutable objects (ie. Maps) have keys and values of a single certain type.

Which means, if you had a TypeScript object modeled like this:


interface IGeographicCoordinate {
    lat: number;
    lng: number;
}

interface IPlacemark {
    id: number;
    coordinate: IGeographicCoordinate;
    name: string;
}

It's immutable form would look like this:


import { fromJS, Map } from 'immutable';

...

let pm: IPlacemark = {
    id: 1,
    coordinate: {
        lat: 0.0,
        lng: 0.0
    },
    name: "Null Island"
};
//First way: using fromJS
let imPlacemark = fromJS(pm);
//Second way: using Map()
let imPlacemark2 = Map(pm);

Now if we were to have a peek at what the inferred types are in any editor that supports TypeScript (for example, Visual Studio Code), we see that the default tooling experience is ... not very helpful



The first immutable object is inferred to be of type 'any'. The second one is inferred to be of type 'Map<{}, {}>', which is close enough to 'any'. If you're working with objects of type 'any', you're pretty much back in vanilla JS land with none of the benefits that TypeScript offers. If an object's type or shape can be described in TypeScript, it should!

Sadly, the typings in Immutable.JS demand our Map instances use homogeneous key and value types for all possible properties. Having keys and values as of type 'any' doesn't really help us in the strongly-typed land of TypeScript. 'any' is pretty much an escape hatch type to allow for interoperability with existing JavaScript code/libraries, it offers zero value in terms of type-safety and tooling experience.

Not to mention, accessing values of such an immutable object is magic strings galore.


let id = imPlacemark.get('id');

And because we're dealing with 'any', that propagates down to everything.



When you have nested objects, you're pretty much back in JavaScript land where any object can be anything. TypeScript can't help us here. You have to know up-front what types you're expecting because the TypeScript tooling and language services won't be able to help you.


//Returns Immutable.Map and not IGeographicCoordinate
let coord = imPlacemark.get('coordinate');
//These will be any and you have to know to get on 'lat' and 'lng' keys
let lat = coord.get('lat');
let lng = coord.get('lng');

So is there anything in TypeScript that allows us to work with APIs such as Immutable.JS Maps in a more rigid and robust manner? Fortunately, I've found a useful little pattern that I've been using that makes working with immutable objects more TypeScript friendly and it involves leveraging 2 language features of TypeScript:
With these two features, we can define a complementary (and mostly type-safe) immutable version of any interface that will return different types based on the name of the key we pass in. So if we return to our placemark example.


interface IGeographicCoordinate {
    lat: number;
    lng: number;
}

interface IPlacemark {
    id: number;
    coordinate: IGeographicCoordinate;
    name: string;
}

We can define complementary immutable versions of the above interfaces like so:


type GeographicCoordinateProperties = "lat" | "lng";
type PlacemarkProperties = "id" | "coordinate" | "name";

interface IGeographicCoordinateImmutable {
    get(key: GeographicCoordinateProperties): any;
    get(key: "lat"): number;
    get(key: "lng"): number;
}

interface IPlacemarkImmutable {
    get(key: PlacemarkProperties): any;
    get(key: "id"): number;
    get(key: "coordinate"): IGeographicCoordinateImmutable;
    get(key: "name"): string;
}

How this works is as follows:
  • We define string literal types for each interface that contains the names of all the properties of that interface.
  • In each complementary immutable interface, the get() method that returns 'any' is just a signature that closely matches the get() method of the original Immutable.JS Map. Because interfaces in TypeScript are just a compile-time means of enforcing and validating an object's "shape", it doesn't have to precisely match 1:1 with get() in Map. It just has to take a key and return some value, this interface type information disappears once everything is transpiled to JavaScript. This method signature is just the proverbial "foot in the door" to allow us to define specialized overload signatures off of it, we don't actually use this signature. By having the key be of a string literal type, it constrains our specialized overload signatures to only property names of the actual interface.
  • Then it is just a case of filling out each specialized get() with the expected return type for the given property name. If a property is another nested object, you basically return its "immutable" equivalent interface.
With this pattern, you can safely "cast" your Immutable.JS maps into type-safe immutable versions of your original interfaces with full TypeScript tooling assistance.



And you will be prevented from accessing values with unknown keys due to the string literal type constraint.



Now, unfortunately there is still one problem with this approach. We're still using magic strings for property access (albeit, constrained to a specific set of string values), meaning this is not resilient against rename or other structural reorganisation refactorings (you'll have to manually update any changed property names in the immutable interfaces), which is why I am so hoping that TypeScript gets something like a nameof operator in a future release. Having a nameof-like language construct should mean the death of most magic strings in your codebase, and would make this immutable interface approach completely type safe and refactoring-friendly. 

Still, the above pattern in its current form has greatly simplified my usage of Immutable.JS objects in TypeScript, which itself has already simplified the building of React applications and components.

Hope this is useful to you as it was for me.