My function has a straightforward task in theory, but its type description is lacking, leading to the need for type assertions whenever I work with the data.
The function:
const fetch_and_encode = <T, E extends Encoded, C>({ source, encode, context }: {
source: Fetcher<T | E> | T | E,
encode?: Encoder<T, E, C>,
context?: C
}): E => {
let decoded;
if ( typeof source === 'function' ) {
decoded = (source as Fetcher<T | E>)();
} else {
decoded = source;
}
if ( typeof encode === 'function' ) {
return encode(decoded as T, context);
} else {
return decoded as E;
}
};
The defined types:
export type Encoded = number | string | ArrayBuffer // | Array<Encoded> | Map<string, Encoded>
export type Fetcher<T> = () => T;
export type Encoder<T, E extends Encoded, C> = (decoded: T, context?: C) => E;
This function essentially deals with two variables, source
and encode
, each with two possible types, resulting in four potential states. The variable source
can be either a piece of data or a function that retrieves data. On the other hand, encode
could be undefined or a function that transforms the output of source
. Ultimately, the combination should produce a value of a relatively simple type, Encoded
.
I've made several attempts to refine the type definition without success, constantly requiring type assertions. These efforts were more about deepening my knowledge of the type system rather than just cleaning up definitions. It seems like there should be a way to specify the types tightly enough to avoid the need for these assertions, and I'd like to figure out how.
One approach I tried using unions didn't actually enhance the type definition:
const fetch_and_encode = <T, E extends Encoded, C>( {source, encode, context}: {
source: Fetcher<T>;
encode: Encoder<T, E, C>;
context?: C;
} | {
source: Exclude<T, Function>;
encode: Encoder<T, E, C>;
context?: C;
} | {
source: Fetcher<E>;
encode: undefined;
context?: any;
} | {
source: E;
encode: undefined;
context?: any;
}): E => {
let decoded;
if ( typeof source === 'function' ) {
decoded = (source as Fetcher<T | E>)();
} else {
decoded = source;
}
if ( typeof encode === 'function' ) {
return encode(decoded as T, context);
} else {
return decoded as E;
}
};
Another attempt using conditional types also hit a dead end:
const fetch_and_encode = <T, E extends Encoded, C>({ source, encode, context }: {
source: Fetcher<T | E> | T | E,
encode: Encoder<T, E, C> | undefined,
context: C | undefined
} extends { source: infer S, encode: infer N, context?: C }
? S extends Function
? S extends Fetcher<T | E>
? N extends undefined
? { source: Fetcher<E>; encode: undefined; context?: any; }
: { source: Fetcher<T>; encode: Encoder<T, E, C>; context?: C; }
: never
: N extends undefined
? { source: E; encode: undefined; context?: any; }
: { source: T; encode: Encoder<T, E, C>; context?: C; }
: never
): E => {
let decoded;
if ( typeof source === 'function' ) {
decoded = (source as Fetcher<T | E>)();
} else {
decoded = source;
}
if ( typeof encode === 'function' ) {
return encode(decoded as T, context);
} else {
return decoded as E;
}
};
I'm at a loss on where to go from here.
Following the suggestion by Ingo Bürk, I experimented with overloads which resolved the initial issues but introduced a new problem that puzzles me:
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
source: E;
encode: undefined;
context?: any;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
source: Fetcher<E>;
encode: undefined;
context?: any;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
source: Fetcher<T>;
encode: Encoder<T, E, C>;
context?: C;
}): E;
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
source: Exclude<T, Function>;
encode: Encoder<T, E, C>;
context?: C;
}): E {
let decoded;
if ( typeof source === 'function' ) {
decoded = source();
} else {
decoded = source;
}
if ( typeof encode === 'function' ) {
return encode(decoded, context);
} else {
return decoded;
}
}
If I include my current generic definition as the default, the error mentioned above disappears, but then I'm back to needing type assertions once again.