typescriptdestructuringconditional-typesgeneric-type-parameters

Conditional type for an array to provide eirther an Error or a generic when destructured


In my Typescript project I have a Result type that I return from functions, containing either an error or some data. It can be either of the form [Error, null], or [null, Data]. For example:

type Result<Data> = [ Error | null, Data | null ]

function randomSuccess(): Result<string> {
    if ( Math.random() > 0.5 ) {
        return [ null, 'success' ]
    else {
        return [ new Error( 'oh no' ), null ]
    }
}

const [ err, result ] = randomSuccess()
if ( err ) {
    .... // can now handle the extracted error and result

I would like Typescript to check that only one of the Error or Data is ever not null. For example:

    ...
        return [ new Error( 'oh no' ), 'success' ]

should throw an error.

My initial attempt at writing a type for this was with conditional types:

type Result<Data> = null extends Error ? [ null, Data ] : [ Error, null ]

This compiles fine when the function returns an error. However when returning valid data - for example return [ null, 'success' ] - the compiler complains:

Type 'null' is not assignable to type 'Error'

I think I understand the compiler error: in my type definition Error is not a parameter so null extends Error will always be false. However I don't know where to go from here.

How can I make a type that is either [Error, null], or [null, Data], and never [Error, Data]?


Solution

  • You're looking for a union type where the parts of the union are the two tuples (arrays) that represent the two possible states:

    type Result<Data> =
        [ null, Data ]      // Success
        |
        [ Error, null ];    // Failure
    

    A Result<Data> can either have one form or the other, but can't have null in both places, and can't have Error, Data. (Also can't have a length other than 2.)

    Here's an example:

    type Result<Data> =
        [ null, Data ]      // Success
        |
        [ Error, null ];    // Failure
        
    function random(): Result<Date> {
    //                        ^^^^−−−− Using `Date` for the `Data` type  argument
    //                                just for example
        if (Math.random() < 0.5) {
            // Fail
            return [new Error(), null];
        }
        // Succeed
        return [null, new Date()];
    }
    
    const a = random();
    if (a[0]) {
        // TypeScript now knows that `a[0]` is an `Error`
        console.log(`Error: ${a[0].message}`);
    } else {
        // TypeScript now knows that `a[1]` is a `Date`
        console.log(`Success at ${a[1].toString()}`);
    }
    

    Playground link