JavaScript and Type Narrowing Magic

Type Narrowing allows the IDE (development environment) to narrow down the object type, from a union type, to a more specific type.
Bullsh*t Example
In this bullsh*t example, value
is the union type string | number
, meaning it can either be a string
or number
(ya, please don’t do that).
The type can be narrowed down by checking the type with an if
. Now that the IDE knows the type, it also knows .toUpperCase()
only exists on string
and .toFixed()
only exists on number
.
Type Narrowing an API Response
In this example, fetchData
can respond with either a Result
or ErrorMessage
type. We can use an if
again to narrow the type down. The IDE now knows inside the if
, data
is always of type Result
and inside the else
, it must be of type ErrorMessage
.
The IDE now gives us proper Property does not exist
error messages when used on the wrong type.
Strict Null Checks
When Strict Null Checks are enabled, we can narrow down data.message
type from a Nullable string
to string
. The error seen on line 17
doesn’t appear on line 20
because the if
narrows down the type.
Type Narrowing Magic in Custom Classes
Type Narrowing can also be controlled in custom classes. Inside the if
, the IDE knows res
is of type Success
and in the else
it is type Failure
.
The type Narrowing is controlled through the JSDocs on the isOk()
function like this:
export class Success {
/** @returns {this is Success<T>} */
isOk() {
return true;
}
}
export class Failure {
/** @returns {this is Success<any>} */
isOk() {
return false;
}
}
Here’s the full source to the above example:
// @ts-check
/**
* @template T
* Represents a successful result.
*/
export class Success {
/** @param {T} value */
constructor(value) {
/** @private */
this._value = value;
}
/** @returns {this is Success<T>} */
isOk() {
return true;
}
/** @returns {T} */
value() {
return this._value;
}
}
/**
* @template E
* Represents a failed result.
*/
export class Failure {
/** @param {E} error */
constructor(error) {
/** @private */
this._error = error;
}
/** @returns {this is Success<any>} */
isOk() {
return false;
}
/** @returns {E} */
error() {
return this._error;
}
}
/**
* Result class with static helpers for Success and Failure.
*/
export default class Result {
/**
* @template T
* @param {T} value
* @returns {Success<T>}
*/
static success(value) {
return new Success(value);
}
/**
* @template E
* @param {E} error
* @returns {Failure<E>}
*/
static failure(error) {
return new Failure(error);
}
}
const res = Math.random() > 0.5 ? Result.success(42) : Result.failure("error");
if (res.isOk()) {
console.log(res.value());
console.log(res.error());
} else {
console.log(res.value());
console.log(res.error());
}
Summary
Type Narrowing can be used to narrow down a type from a union type to a specific type. This allows the IDE to know which type you are working with and give errors or warnings about incorrect usage allowing you to write safer code.
Cheers 🍻
Subscribe to my newsletter
Read articles from Joel Thoms directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
