Reputation: 39
I'm trying to type the return value of multiple consecutive transform functions. While my typing works for the result itself and the first transforming method. The typing doesn't work for the second transform method.
playground ts link to fiddle around.
type Fn<Input = any, Output = any> = (input: Input) => Output
const transform = <
StoredValue extends ReturnType<Store>,
Fetch extends Fn<any, unknown>,
FetchedValue extends ReturnType<Fetch>,
SelectOutput extends ReturnType<Select>,
Store extends Fn<FetchedValue, unknown>/* = Fn<FetchedValue, FetchedValue>*/,
Select extends Fn<StoredValue, unknown>/* = Fn<StoredValue, FetchedValue>*/,
>(config: {
fetch: Fetch,
store/*?*/: Store,
select/*?*/: Select,
}) => {
return {} as {
fetchedValue: FetchedValue,
storedValue: StoredValue,
selectedValue: SelectOutput,
}
}
const ret = transform({
fetch: (a: number) => {
return 'fetchedValue' as 'fetchedValue'
},
store: (b) => {
return 'storedValue' as 'storedValue'
},
// StoredValue === unknown, but should be 'storedValue'
select: (input) => {
return 'selectedValue' as 'selectedValue'
},
})
Upvotes: 1
Views: 49
Reputation: 329523
It usually helps to express types in terms of simple type operations on few generic type parameters.
It's more straightforward to express function types in terms of their argument and return types (e.g., turn I
and O
into (i: I) => O
) than to do the reverse (e.g., turn (i: I) => O
into I
and O
), which requires more complicated type operations involving conditional types like the ReturnType<T>
utility type.
Given your use cases, the underlying types you care about seem to be:
fetch
, FI
(maybe)fetch
and the input type to store
, FO
store
and the input type to select
, SV
, andselect
, SO
.So that leads me to the following typing for transform()
:
const transform = <FI, FO, SV = FO, SO = FO>(
config: {
fetch: (arg: FI) => FO,
store?: (arg: FO) => SV,
select?: (args: SV) => SO
}) => {
return {} as {
fetchedValue: FO,
storedValue: SV,
selectedValue: SO,
}
}
or possibly just
const transform = <FO, SV = FO, SO = FO>(
config: {
fetch: (arg: any) => FO,
store?: (arg: FO) => SV,
select?: (args: SV) => SO
}) => {
return {} as {
fetchedValue: FO,
storedValue: SV,
selectedValue: SO,
}
}
if you don't actually need the input type to fetch()
anywhere (the example code doesn't use that input type, but your underlying use case might).
Note that in order to support the use cases where either store
or select
are not passed in and therefore SV
and/or SO
cannot be inferred from these arguments, I've given SV
and SO
default type arguments of FO
. I'm not 100% sure that is always what you want to do, so you should double check to make sure it works as desired and adjust accordingly.
Also, I'm not worried about having the compiler verify the type safety of the implementation of transform()
. Both your version and mine return {}
and assert that it is of the proper type. Having the compiler verify type safety for generic type parameters with default type arguments isn't usually possible, so you'll probably need to use a type assertion or something like it in your actual code as well.
Okay, let's make sure it works as you expect:
const ret = transform({
fetch: (a: number) => 'fetchedValue' as const,
store: (b) => 'storedValue' as const,
select: (input) => 'selectedValue' as const
})
// const transform: <number, "fetchedValue", "storedValue", "selectedValue">(...
/* const ret: {
fetchedValue: "fetchedValue";
storedValue: "storedValue";
selectedValue: "selectedValue";
} */
const ret2 = transform({
fetch: (a: number) => 'fetchedValue' as const,
select: (input) => 'selectedValue' as const
})
// const transform: <number, "fetchedValue", "fetchedValue", "selectedValue">(...
/* const ret2: {
fetchedValue: "fetchedValue";
storedValue: "fetchedValue";
selectedValue: "selectedValue";
} */
Looks good. The type of ret
and ret2
are correct, and the inferred type arguments for FI
, FO
, SV
, and SO
match what I think you're looking for.
Upvotes: 1