Execute Program

Everyday TypeScript: Error Handling With Unions

Welcome to the Error Handling With Unions lesson!

This lesson is shown as static text below. However, it's designed to be used interactively. Click the button below to start!

  • We'll use a common type of JavaScript bug to explore error handling with unions. JavaScript has a Number function that converts strings like '1' into numbers like 1.

  • >
    Number('1');
    Result:
    1Pass Icon
  • When we give Number a string that isn't a number, it returns NaN.

  • >
    Number('this is not a number');
    Result:
    NaNPass Icon
  • NaN is a special value in JavaScript that means "not a number". TypeScript follows JavaScript by allowing NaN values in variables of type number. That can allow NaN to sneak into code where we only expected actual numbers.

  • Unexpected NaNs will often flow through the system. For example: NaN plus anything is NaN, NaN minus anything is NaN, etc. The NaN can eventually cause a runtime error in a module far away from the original source of the bug. Or, worse, the NaN might make it all the way into a database.

  • >
    NaN + 1;
    Result:
    NaNPass Icon
  • >
    NaN * NaN;
    Result:
    NaNPass Icon
  • >
    function square(n: number): number { return n * n; }
    square(NaN);
    Result:
    NaNPass Icon
  • Let's write safeNumber, a safer version of the Number conversion function that doesn't return NaN.

  • We could make safeNumber throw an exception when we try to convert a string that isn't a valid number. However, that's generally a bad idea for conversion functions like safeNumber. When converting strings that come from users, we have to expect failure and handle it gracefully.

  • Instead of throwing an exception, we can return a union. One side of the union indicates success: we converted the string to a number. The other side represents failure: the string wasn't actually a number. First, we'll define the ConversionResult union type:

  • >
    type ConversionSucceeded = {
    kind: 'success'
    value: number
    };

    type ConversionFailed = {
    kind: 'failure'
    reason: string
    };

    type ConversionResult = ConversionSucceeded | ConversionFailed;
  • Internally, safeNumber will use the regular Number function. It will also use the built-in JavaScript function Number.isNaN to check for NaNs. (We can't check for NaNs with === NaN because NaN doesn't equal itself.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    function safeNumber(s: string): ConversionResult {
    const n: number = Number(s);
    if (Number.isNaN(n)) {
    return {kind: 'failure', reason: 'conversion returned a NaN'};
    } else {
    return {kind: 'success', value: n};
    }
    }
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    safeNumber('1');
    Result:
    {kind: 'success', value: 1}Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    safeNumber('this is not a number');
    Result:
    {kind: 'failure', reason: 'conversion returned a NaN'}Pass Icon
  • Now we can see why our safeNumber is safe. It returns an object, and objects can't be added to numbers. If we try to do safeNumber(someString) + 1, it will be a type error. (You can type type error when a code example will fail with a type error.)

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    safeNumber('5') + 1;
    Result:
    type error: Operator '+' cannot be applied to types 'ConversionResult' and 'number'.Pass Icon
  • The converted number is in the value property of the returned object. However, that value property only exists on the success side of the ConversionResult union. If we try to do safeNumber(someString).value, we'll get a type error.

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    // Think carefully about safeNumber's return type here.
    safeNumber('1').value;
    Result:
    type error: Property 'value' does not exist on type 'ConversionResult'.
      Property 'value' does not exist on type 'ConversionFailed'.Pass Icon
  • This may seem wrong at first. We saw in an earlier example that safeNumber('1') returns {kind: 'success', value: 1}. That object has a value property, but when we tried to access .value in the example above we got a type error. Why won't TypeScript let us access the value property that clearly exists?

  • The answer lies in the distinction between types and runtime values. We only know that safeNumber('1') has a value property because we can actually call the function. But the TypeScript compiler can't call functions, so it can't know the exact value that safeNumber('1') will return. TypeScript only knows the types. Specifically, it knows that safeNumber returns a ConversionSucceeded | ConversionFailed, and that only one of those alternatives has a value property.

  • Here's another way to think about the same thing. Sometimes, safeNumber returns an object with no value. If TypeScript allowed us to access .value on the object, then we'd sometimes be accessing a property that doesn't exist, which would be a bug!

  • Given that, how can we access .value? We have to use an if, a ternary conditional (? and :), or a switch to check the result. Did we get a ConversionSucceeded (with a value) or a ConversionFailed (without a value)?

  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const converted = safeNumber('5');
    converted.kind === 'success' ? converted.value : undefined;
    Result:
    5Pass Icon
  • Note: this code example reuses elements (variables, etc.) defined in earlier examples.
    >
    const converted = safeNumber('this is not a number');
    converted.kind === 'success' ? converted.value : undefined;
    Result:
    undefinedPass Icon
  • This works because the ternary conditional is acting as a type guard. Inside the "true" branch of the conditional, the TypeScript compiler knows that converted.kind === 'success'. That tells it that converted is a ConversionSucceeded, so it must have a value attribute.

  • By returning a union from safeNumber, we've forced callers to deal with the error case. In the ternary above, we return undefined if safeNumber fails. In a full application, we might show a "that wasn't a valid number" error to the user instead. The one thing that we can't do is accidentally continue on without handling the error; the type system guarantees that we won't make that mistake.