Search code examples
typescript

Unable to share discriminated union type and generic types


I'm using TypeScript 5.4.5, and I have the following utility helpers:

type Result<T, E> = [T, null] | [null, E];

function ok<T>(good: T): Result<T, null> {
    return [good, null];
}

function err<E>(bad: E): Result<null, E> {
    return [null, bad];
}

I have a utility function using the Result type for fetch responses that compiles, no TypeScript errors:

type FetchResult = Result<Response, Error>;
async function tryFetch(fetch: () => Promise<Response>): Promise<FetchResult> {
    try {
        const result = await fetch();
        if (!result.ok) {
            return [null, new Error(`Failed to fetch: ${result.statusText}`)];
        }
        return [result, null];
    } catch (error) {
        return [null, error as Error];
    }
}

Now I have the following helper function, which attempts to use the same type system but shows a ts error:

type FetchJsonResult<T> = Result<T, Error>;
async function tryFetchJson<T>(fetchFn: () => Promise<Response>): Promise<FetchJsonResult<T>> {
    try {
        const response = await fetchFn();
        if (!response.ok) {
            return err(new Error(`Failed to fetch: ${response.statusText}`));
        }
        const data: T = await response.json();
        return ok(data);
    } catch (error) {
        return err(error as Error);
    }
}

The errors are as follows:

Type 'Result<null, Error>' is not assignable to type 'FetchJsonResult<T>'.
  Type '[null, null]' is not assignable to type 'FetchJsonResult<T>'.
    Type '[null, null]' is not assignable to type '[T, null]'.
      Type at position 0 in source is not compatible with type at position 0 in target.
        Type 'null' is not assignable to type 'T'.
          'T' could be instantiated with an arbitrary type which could be unrelated to 'null'.

and

Type 'Result<T, null>' is not assignable to type 'FetchJsonResult<T>'.
  Type '[null, null]' is not assignable to type 'FetchJsonResult<T>'.

and

Type 'Result<null, Error>' is not assignable to type 'FetchJsonResult<T>'.

Would there be a better way to accomplish the above with a single implementation of ok and err while avoiding a cast to FetchJsonresult?


Solution

  • Why not to simplify and allow TS to infer the return type?:

    Playground

    function ok<T extends object>(good: T): [T, null] {
        return [good, null];
    }
    
    function err<E extends Error>(bad: E): [null, E] {
        return [null, bad];
    }
    
    async function tryFetchJson<T extends object>(fetchFn: () => Promise<Response>) {
        try {
            const response = await fetchFn();
            if (!response.ok) {
                return err(new Error(`Failed to fetch: ${response.statusText}`));
            }
            const data: T = await response.json();
            return ok(data);
        } catch (error) {
            return err(error as Error);
        }
    }
    
    const r = tryFetchJson(() => fetch('domain') ); // const r: Promise<[null, Error] | [object, null]>
    
    

    I would even use as const:

    function ok<T extends object>(good: T) {
        return [good, null] as const;
    }
    
    function err<E extends Error>(bad: E) {
        return [null, bad] as const;
    }