Alexander Farkas
Alexander Farkas

Reputation: 39

Typing generic return data based on multiple + optional consecutive transform functions

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

Answers (1)

jcalz
jcalz

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:

  • the input type to fetch, FI (maybe)
  • the output type of fetch and the input type to store, FO
  • the output type of store and the input type to select, SV, and
  • the output type of select, 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.

Playground link to code

Upvotes: 1

Related Questions