Typescript and a Result Type?
I like the idea of a Result
type in Typescript as I’ve used it many times in Rust (in fact, it’s very difficult to write code without it). But, in TypeScript, I’m not sure that the benefit is as clear. In Rust, the Result
type helps with error handling and propagating errors up the call stack. But in TypeScript, we already have exceptions for that purpose.
I realize this post is about mishandling never
in TypeScript and not about Result
types specifically, but it’s still a good read:
The never
type and error handling in TypeScript
Here’s an example from Stefan’s post where he’s coded a function that doesn’t return a number
or an Error
:
const result = divide(10, 0);
if (result.kind === "error") {
// result is of type Error
console.error(result.error);
} else {
// result is of type Success<number>
console.log(result.value);
}
Instead the divide
function returns a Result type which has a kind
property and either a value
or an error
property.
Needing to pull the return from a value property, in a temporary object to handle a rare error case seems like an unnecessary syntax burden in JavaScript. While it’s possible modern JavaScript compilers can mostly optimize the extra overhead of the returned object (as it has a kind
property with a string value), it comes with a runtime cost. Is it much? No, not when isolated and infrequent. But, it’s a cost that could be avoided with good underlying design and practices.
In Rust, the Result
is an enum. Result<T, E>
has two states: Err(E)
and Ok(T)
. The structure is no larger than the memory needed for the larger of T
and E
types plus a single byte to indicate which state the Result
is in.
use std::mem;
fn main() {
let size = mem::size_of::<Result<i32, ()>>();
println!("Size of Result<i32, ()>: {} bytes", size); // > 8 bytes
}
You might not think this matters, but it can matter in large code bases, or code that is executed frequently (especially framework code). If modern web apps weren’t so frequently slow, I’d be less concerned.
Don’t forget that you can document a throw
using JSDoc as well.
/**
* Divides a number by another number but checks if the divisor is zero
* @param a first number
* @param b Divides a by b
* @throws Will throw if b is zero
*/
function divide(a: number, b: number): number
{
if (b === 0) {
throw new Error("b cannot be 0")
}
return a/b
}