Typescript and a Result Type?

3 min read

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:

typescript

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.

rust
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.

typescript
/**
 * 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
}