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?
Why not to simplify and allow TS to infer the return type?:
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;
}