First, sorry, it was really hard to define my question.
I have as string like this: "first.second.third"
and some value
(it can be object, boolean or string). I wrote reduce
function which converts those two variables in object, it looks like this:
{ first: { second: { third: value } } }
here is my function:
const processNestedFilter = (columnField, value) => {
const fields = columnField.split('.').reverse();
const items = [value, ...fields];
const nestedFilter = items.reduce((filter, current, index) => {
if (index === 0) {
return current;
}
return { [current]: filter };
}, {});
return nestedFilter;
};
I tried to write types for it, but failed. I'm not good yet with recursive type definitions in typescript. May someone explain, how you should start your type creation for such not simple functions? And could anyone help me with a type for it?
By reverse, I mean change of accumulator in reduce function to value and currentValue to key.
You may see how it works here: https://codesandbox.io/s/affectionate-cache-wnlnuo?file=/src/index.js
Thank you a lot!
CodePudding user response:
So we need to come with a description of what processNestedFilter(columnField, value)
produces at the type level. Let's call the type ProcessNestedFilter<K, V>
and make it the return type of the function:
declare const processNestedFilter: <K extends string, V>(
columnField: K, value: V) => ProcessNestedFilter<K, V>;
So K
is the string literal type corresponding to columnField
, like "first.second.third"
in your example; while V
is the type corresponding to value
.
We can write ProcessNestedFilter<K, V>
as a recursive conditional type using template literal types to split the K
type at the dot character "."
:
type ProcessNestedFilter<K extends string, V> =
K extends `${infer F}.${infer R}` ?
{ [P in F]: ProcessNestedFilter<R, V> } : { [P in K]: V }
If K
can be split into a first chunk F
, followed by a dot "."
, followed by the rest of the string R
, then we want ProcessNestedFilter<K, V>
to be an object type with F
as a key, and whose value type is ProcessNestedFilter<R, V>
. We can write that as the mapped type {[P in F]: ProcessNestedFilter<R, V>}
; this is equivalent to Record<F, ProcessNestedFilter<R, V>>
using the Record<K, V>
utility type.
On the other hand, if K
cannot be split this way, then it's a single key, and we want ProcessNestedFilter<K, V>
to be an object type with K
as a key and with V
as a value. We can write that as the mapped type {[P in K]: V }
; this is equivalent to Record<K, V>
.
Let's test it out:
const x = processNestedFilter("first.second.third", 123);
/* const x: {
first: {
second: {
third: number;
};
};
} */
Looks good. The type of x
is {first: {second: {third: number}}}
as desired.
Note that the compiler cannot figure out that your implementation of processNestedFilter
satisfies the call signature. It's too complicated of a type function. So you will need to use type assertions or something similar to convince the compiler not to warn you about the implementation. That also means you need to be very careful to check that your implementation does what you say it does, because the compiler can't help.
It might look like this:
const processNestedFilter = <K extends string, V>(
columnField: K, value: V) =>
columnField.split('.').reverse().reduce<any>((filter, current) => {
return { [current]: filter };
}, value) as ProcessNestedFilter<K, V>;
Here I am saying that reduce()
will produce the any
type because I don't want the compiler to worry about it. And then at the end I just assert that the return type is ProcessNestedFilter<K, V>
.
And there you go:
console.log(x);
/* {
"first": {
"second": {
"third": 123
}
}
} */