
06 Jan 2017 Writing safer code with TypeScript strict null checks & type guards
In this article I will show you how to write safer TypeScript code by leveraging a feature called strictNullChecks. It’s quite easy to miss because it’s turned off by default, but can help a lot to produce more robust code. I’ll also briefly introduce another lesser-known feature of the language called type guards.
Some Java full-stack developers (like me) always wanted to have statically typed JavaScript. I remember when starting a new project with GWT and being quite amazed by the possibility of using Java on both sides.
Nowadays, many new languages are trying to be a replacement of JavaScript. TypeScript is one of them. I got my first experience with TypeScript when trying early betas of Angular 2. I quite liked a concept of adding static types to JavaScript. However, I also see developers trying to keep the freedom of JavaScript. Fortunately, TypeScript gives developers flexibility to decide what way they want to go and how they want to mix static vs dynamic types.
To experiment with these tradeoffs, I decided to use TypeScript for a new React/Redux project. The application is a web SPA which is the front end for a typical SAAS. Users can register/login, adding credit cards, managing api keys, see billing information, etc. All the examples in this article will be from that project and have React+Redux context.
Problem overview
One of the pain points of JavaScript is nulls and undefined values. I won’t deep dive into the problem as it has been described and discussed many times. Suffice to say that any developer who has tried to access an object that was null or undefined will probably recognise these sorts of error message:
Uncaught ReferenceError: foo is not defined Uncaught TypeError: window.foo is not a function
These errors happen at runtime when you’ve deployed your code already. Ideally, we want to prevent them when writing the code, not when we are running it. That’s what strictNullChecks compiler flag is for.
Ideally, if you read the TypeScript manual before coding, you’d know about this flag already. In my case I was learning TypeScript code-first and only learnt about it after the fact. So, my first suggestion would be this: don’t be me, RTFM first! The TypeScript handbook is really tiny and handy. You can finish it in 2-3 days.
Strict null checks
strictNullChecks protects you from referencing nulls or undefined values in code. It can be enabled by adding the –strictNullChecks flag as an option to the command-line compiler, or adding it to tsconfig.json file.
The TypeScript developers actually encourage us to turn this option on. From the handbook:
As a note: we encourage the use of –strictNullChecks when possible, but for the purposes of this handbook, we will assume it is turned off.
It’s a good idea to enable it when your project starts. Otherwise you can end up with hundreds of compiler errors when you finally turn it on. By the time I got around to it enabling it, it took me about 3 hours to fix all of the resultant errors.
Let’s have a look at an example that will fail during compilation when flag is on (it compiles fine when flag is off):
function mapStateToProps(state: State, ownProps?: TApiKeyProps): TApiKeyProps { | |
return { | |
apiKeys: state.apiKey.data.apiKeys, | |
//ERROR: TS2532: Object is possibly 'undefined'. | |
onAddApiKey: ownProps.onAddApiKey, | |
//ERROR: TS2532: Object is possibly 'undefined'. | |
onDeleteApiKey: ownProps.onDeleteApiKey | |
} as TApiKeyProps | |
} |
The compiler gives us an error because `ownProps` is an optional argument and could be undefined. So this is the point where compiler is telling us: “Dude, watch for undefined!”.
To fix the problem we can use an upcoming ECMAScript feature called object spread that is implemented in TypeScript 2.1:
function mapStateToProps(state: State, ownProps?: TApiKeyProps): TApiKeyProps { | |
return { | |
//Copies all fields from ownProps and overwrite with fields defined below (apiKeys) | |
...ownProps, | |
apiKeys: state.apiKey.data.apiKeys | |
} as TApiKeyProps | |
} |
If `ownProps` is undefined then it will be just ignored. The important point is that we are prevented from accessing an object that could be undefined. Alternately, we would explicitly check `ownProps` to be defined (in an if condition) without object spreading syntax.
The next example shows how strictNullChecks could help not only make code safer but also reduce some boilerplate:
export function apiKey(state: ApiKeyState, action: BaseAction) : ApiKeyState { | |
let newState: ApiKeyState; | |
switch (action.type) { | |
case 'ADD_API_KEY': | |
newState = Object.assign({}, state); | |
newState.data.apiKeys = [ | |
...state.data.apiKeys, | |
action.payload.Key | |
] as string[]; | |
break; | |
//More actions handling here | |
//... | |
} | |
//ERROR: TS2454:Variable 'newState' is used before being assigned. | |
if (newState) { | |
return newState; | |
} else { | |
return state ? state : API_KEY_INITIAL_STATE; | |
} | |
} |
The compiler is failing on line 17 because we are trying to use a `newState` variable, but it’s possibly was never assigned (ah, that sweet uninitialised identifier). So to fix the error we can assign `newState` to `state` from the passed in arguments, and remove the `if` condition entirely:
export function apiKey(state: ApiKeyState, action: BaseAction) : ApiKeyState { | |
//Assigning variable here. | |
let newState: ApiKeyState = state; | |
switch (action.type) { | |
case 'ADD_API_KEY': | |
newState = Object.assign({}, state); | |
newState.data.apiKeys = [ | |
...state.data.apiKeys, | |
action.payload.Key | |
] as string[]; | |
break; | |
//More actions handling here | |
//... | |
} | |
//Using only newState in return. | |
return newState ? newState : API_KEY_INITIAL_STATE; | |
} |
That said, in some cases you want to use null or undefined values. For instance:
private usedMb(): number { | |
if (this.props.usedBytes != null) { | |
return (this.props.usedBytes / (1024 * 1024)); | |
} | |
//ERROR: TS2322:Type undefined is not assignable to type number. | |
return undefined; | |
} |
So in this case, returning undefined is a valid behaviour of the function. However, the compiler generates an error. To work around it, we need to explicitly add undefined to the function declaration to let the compiler know that it’s a valid case:
private usedMb(): number | undefined { | |
if (this.props.usedBytes != null) { | |
return (this.props.usedBytes / (1024 * 1024)); | |
} | |
return undefined; | |
} |
Now, if we try to use the function without checking the result, we will get additional errors. For example:
private formatUsedMb(): string { | |
//ERROR: TS2531: Object is possibly undefined | |
return this.usedMb().toFixed(0).toString(); | |
} |
So in this case we should check that the return value is defined:
private formatUsed(): string { | |
let usedMb = this.usedMb(); | |
return usedMb ? usedMb.toFixed(0).toString() : ''; | |
} |
NOTE: I found that using undefined instead of null is more convenient, because TypeScript uses undefined in place of optional interfaces fields and function arguments.
Type guards
Type guards are another TypeScript feature that allows us to check types and automatically resolve them.
Consider this simple function that converts bytes to megabytes:
private usedMb(): number | undefined { | |
if (this.props.usedBytes) { | |
return (this.props.usedBytes / (1024 * 1024)); | |
} | |
return undefined; | |
} |
We’d like that if `this.props.usedBytes` is undefined, then usedMb() will return undefined. However, our current implementation won’t work as expected if `this.props.usedBytes` is assigned a zero value, as we’ll get undefined as a returned value when what we actually want is zero. You’d have to rely on unit tests to pick something up like that, but what’d be nice is we could check that the type of the property is a number.
Fortunately, TypeScript provides nice functionality to do that. It’s called a type guard. In our case it’s just a simple function that looks like this:
export function isNumber(n: any): n is number { | |
return typeof n === "number"; | |
} |
By having a return signature of the form ” is “, we’re telling the compiler to consider our function to be a type guard.
Then we can rewrite our original usedMb() function to use our type guard:
private usedMb(): number | undefined { | |
//Using type guard isNumber() | |
return isNumber(this.props.usedBytes) ? | |
(this.props.usedBytes / (1024 * 1024)) : | |
undefined; | |
} |
That said, using ‘any’ as an argument type for a type guard function is actually not that common. Type guards are more powerful when you can declare a range of passing types. The compiler can then distinguish between those types during usage. Consider this example from the handbook:
interface Bird { | |
fly(); | |
layEggs(); | |
} | |
interface Fish { | |
swim(); | |
layEggs(); | |
} | |
function isFish(pet: Fish | Bird): pet is Fish { | |
return (<Fish>pet).swim !== undefined; | |
} | |
... | |
// Both calls to 'swim' and 'fly' are now okay. | |
if (isFish(pet)) { | |
pet.swim(); | |
} | |
else { | |
pet.fly(); | |
} |
As you can see compiler knows that pet is of type Bird when goes to else statement.
Conclusion
TypeScript provides a couple of handy mechanisms for making your code safer.
In particular, I have found the ‘–strictNullChecks’ option to be very useful. It’s highlighted actual problems with my code, without generate any noise. I would highly recommend that you turn it on if you haven’t already. Also, using it tends to make your code not only safer but also smaller and more elegant.
Type guards are also an interesting feature, although to be honest I have less real-world experience with them.
Using these features takes some of the pressure off of your unit test suite to cover all possible scenarios. This doesn’t mean they should replace unit tests. Instead, they can complement them, especially when performing large refactorings.
Pingback:TEL monthly newsletter – Feb 2017 – Shine Solutions Group
Posted at 21:06h, 08 March[…] Pokidov blogged about how to write safer TypeScript code using strict null checks and type guards. Is it really possible to write safe JavaScript? […]
emirotin
Posted at 22:01h, 16 AprilNIT: `toString` is not needed after `toFixed` as it already returns string
Adeeb
Posted at 14:51h, 20 AprilHad no idea about this aspect of TypeScript!
CHRIS GOLDSMITH
Posted at 06:09h, 20 MarchVery good article thanks! One small NIT when would the site post of this ternery operator occur?
return newState ? newState : API_KEY_INITIAL_STATE;
It looks to me that in all the paths that newState will receive a defined value.
ohhako
Posted at 03:15h, 02 JulyHello I’m hako. I’m studying for feature of strictNullChecks. but there’s no information in my language Korea. So i wanna ask that if your’re okay, can i have translate your post and upload in my gitblog? Surely i will leave your source on my post.
Ben Teese
Posted at 09:02h, 02 JulySure Hako, go for it. Maybe also post a comment to this post with a link to the translation.
ohhako
Posted at 17:09h, 02 AugustThank you Ben! it was really helpful to me.
https://ohhako.github.io/articles/2020-07/Angular-strictNullChecks-post