Home > Mobile >  When declaring an array of generic items, how can I allow the generic parameter be inferred?
When declaring an array of generic items, how can I allow the generic parameter be inferred?

Time:11-11

I have a situation where I have an array of generic items (Item), and within the item itself, I want the generic parameter to be inferred and specific.

That is, I want have an array of generic items, but each one can have a different generic typing, and that should be retained.

type Item<T> = {
  value: T; 
  fn: (value: T) => void; 
}


function usesItem<T>(item: Item<T>) {

}


// This is fine - the value is inferred
usesItem({
  value: 999, 
  fn: (value) => {
      //value is inferred as number
  }
})

First approach, declare an Array<Item<unknown>:

function usesItems1(items: Array<Item<unknown>>) {

}


usesItems1([{
  value: 999, 
  fn: (value) => {
    // value is unknown - we want it to be number
  }
}
])

Second approach: introduce the generic from the function:

function usesItems2<T>(items: Array<Item<T>>) {

}

// appears to work...
usesItems2([{
  value: 999, 
  fn: (value) => {
    // value is number
  }
}
])

// ...but it doesn't really
usesItems2([{
  value: 999, 
  fn: (value) => {
    // value is number
  }
}, {
  // Type 'string' is not assignable to type 'number'.(2322)
  value: "aaa", 
  fn: (value) => {

  }
}
]); 

Third approach - use the infer keyword, I tried a couple of approaches:

type InferredItem1<T>  = T extends Item<infer R> ? Item<R> : never; 

function usesItems3(items: Array<InferredItem1<unknown>>) {

}

usesItems3([{
  //Type 'number' is not assignable to type 'never'.(2322)

  value: 999, 
  fn: (value) => {
  }
}
]); 



type InferredItem2<T extends Item<unknown>>  = T extends Item<infer R> ? Item<R> : never; 

function usesItems3b(items: Array<InferredItem2<Item<unknown>>>) {

}

usesItems3b([{
  value: 999, 
  fn: (value) => {
    // value is unknown
  }
}
]); 

TypeScript Playground

How can I achieve what I want?

CodePudding user response:

Well, I have something that works, but it's weird and hacky, and I don't know if I'm right about why it works. The first part is straightforward: the createRoutes function needs to be generic so that there is a type parameter to infer at all:

function createRoutes<T>(routes: Route<T>[]) {
  // ...
}

Here comes the weird part: requestBodyValidator needs to be written as a method instead of an arrow function, but createHandler needs to stay as an arrow function:

createRoutes([{
  route: "/a",
  method: "get",
  // method, not arrow function
  requestBodyValidator(item): Bar {
    return item as Bar;
  },
  // arrow function
  createHandler: () => {
    // value is inferred as Bar 
    return (value) => {}
  }
}])

Playground Link

My best guess for why this works is that by making requestBodyValidator a method, its type is considered when inferring the type of the whole object, because Typescript treats a method as "part of the object" in that sense. Then because createHandler is an arrow function, Typescript treats it like a sub-expression and its type is inferred from context. Because this sub-expression doesn't have a known type while the object's type is being inferred, then its type can't be used to infer the object's type. That's my best guess, anyway; maybe the reason is something else.

CodePudding user response:

Let me know if it works for you:


type Handler<T> = (value: T) => void;

type Methods = "post" | "get" | "put" | "patch" | "delete"

export type Route<T> = {
  methods: Methods,
  requestBodyValidator: (item: unknown) => T;
  createHandler: () => Handler<T>;
}

type CreateHandler<T> = { createHandler: () => Handler<T>; }

type Iterate<T extends ReadonlyArray<Route<any>>, Result extends any[] = []> =
  (T extends []
    ? Result
    : (T extends [infer H, ...infer Tail]
      ? (H extends Route<infer E>
        ? Tail extends ReadonlyArray<Route<any>>
        ? Iterate<Tail, [...Result, Omit<H, 'createHandler'> & CreateHandler<E>]>
        : never
        : never)
      : T)
  )

function createRoutes<
  Type,
  RouteElem extends Route<Type>,
  Routes extends RouteElem[],
  >(routes: Iterate<[...Routes]>) {

}

type Bar = {
  bar: string;
}
type Foo = {
  foo: string;
}

createRoutes([{
  methods: 'get',
  requestBodyValidator(item: unknown): Bar {
    return item as Bar;
  },
  createHandler: () => {

    // I want value to be inferred as Bar 
    return (value /** Bar */) => {

    }
  }
}, {
  methods: 'post',
  requestBodyValidator(item: unknown): Foo {
    return item as Foo;
  },
  createHandler: () => {

    return (value /** Foo */) => {

    }
  }
}])

Playground

P.S. I have shamelesly stolen an idea from @kaya regarding using methods instead of arrow functions.

I was not able to make it work with arrows

  • Related