I have a simple function that has a simple job, in principal, but a poor type description requiring type assertions nearly every time I manipulate 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 referenced 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;
It basically has 2 variables, source and encode, that can each have one of two effective types, leading to 4 states. source is either a datum or function that retrieves a datum. encode is either undefined or a function that transforms the 'result' of source. In the end, this combination must result in a value of (relatively) simple type, Encoded.
I've tried to improve the type definition a couple different ways, but can't seem to avoid needing the type assertions. These attempts have also been as much about deepening my understanding of the type system as cleaning up the actual definitions. That said, it 'feels' like I should be able to specify the types tightly enough to avoid the type assertions and I'd like to understand how.
My first attempt, using unions, didn't seem to actually improve the type definition at all:
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>; // T could still be a subtype of 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>)();
// decoded = source(); // Cannot invoke an expression whose type lacks a call signature. Type 'Fetcher<T> |
// // Fetcher<E> | (Exclude<T, Function> & Function)' has no compatible call signatures.
} else {
decoded = source;
}
if ( typeof encode === 'function' ) {
return encode(decoded as T, context);
// return encode(decoded, context); // Argument of type 'T | E' is not assignable to parameter of type 'T'
} else {
return decoded as E;
// return decoded; // Type 'T | E' is not assignable to type 'E'
}
};
Then I attempted to use actual conditional types, and also got nowhere:
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 // Ensure S isn't a Function if it also isn't a Fetcher
? 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>)();
// decoded = source(); // Cannot invoke an expression whose type lacks a call signature. Type 'Fetcher<T> |
// // Fetcher<E> | (T & Function)' has no compatible call signatures.
} else {
decoded = source;
}
if ( typeof encode === 'function' ) {
return encode(decoded as T, context);
// return encode(decoded, context); // Argument of type 'T | E' is not assignable to parameter of type 'T'
} else {
return decoded as E;
// return decoded; // Type 'T | E' is not assignable to type 'E'
}
};
I'm not sure where else to go from here.
Per the suggestion of Ingo Bürk (below), I tried overloads, and they solved the original issues, but presented a new one that perplexes me:
function fetch_and_encode<T, E extends Encoded, C>({ source, encode, context }: {
// ^^^^^^^^^^^^^^^^ Overload signature is not compatible with function implementation
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>; // T could still be a subtype of 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 add in my current (generic) definition as the default, the above error goes away, but type assertions are once again required.