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
}
}
]);
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) => {}
}
}])
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 */) => {
}
}
}])
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