This is a typescript question, i will try to explain as good as possible. Find a link to a typescript playground with an illustration of the problem at the bottom.
It is about react-hook-form
, which has a type Path<TFieldValues>
(see https://github.com/react-hook-form/react-hook-form/blob/274d8fb950f9944547921849fb6b3ee6e879e358/src/types/utils.ts#L86), which is a string identifier for a possible nested field in dot-notation (i.e. path
, path.subpath
, path.subpath.subpath
etc.) for a given structure TFieldValues
, which are the form values. Its hooks require this Path<TFieldValues>
when specifying a fieldName
, which is desired and nice.
But i have a problem deriving such a Path<TFieldValues>
variable, when i have a structure definition which i recursively iterate. What i am doing is, to traverse a structure definition (that conforms to the TFieldValues
structure) with information on the ui. Then i traverse it and generate the UI for that, using useController
. However i cannot find a way to have a strongly typed Path<TFieldValues>
. You can see in the complete example in the link below, the gist:
// Simplified from react-hook-form, we only care about fieldName typing
const useController = <TFieldValues extends Record<string, any>>(fieldName: Path<TFieldValues>) => {};
// Could be something arbitrary but a fixed structure is given
const myValues = {
number: 1,
string: 'string',
sub: {
number: 2,
sub: {
string: 'string',
},
},
};
// This schema will be recursively walked to generate a UI, it is strongly typed to conform to myValues but that would exceed this post
const schema = {
number: 'Number Field',
string: 'String Field',
sub: {
number: 'Sub Number Field',
sub: {
string: 'Sub Sub String Field',
},
},
};
// This walk function illustrates the problem, as i cannot determine how to derive a valid path when recursive iterating
const walk = (node: Record<string, any>, path?: Path<typeof myValues>) => {
const keys = Object.keys(node);
for (let key in keys) {
if (typeof node[key] === 'string') {
useController<typeof myValues>(path ? `${path}.${key}` : key);
// Show UI etc...
} else {
walk(node[key], path ? `${path}.${key}` : key);
}
}
}
// would be called
walk(schema);
Here is the problem illustrated in the typescript playground: TS Playground
P.S: Please do not suggest using as
, any
or something like that. I know i could cast it to make it work. But i want strong typing.
EDIT
A little edit here, as i was doing some more research. I think the problem can be simplified for better understanding. The main point is appending a new key to Path<X>
and still get a valid Path<X>
in return. If we can solve that, typing the recursive iteration should not be the problem. This essentially can be expressed in a simple function
declare function appendToPath<T>(path: Path<T>, appendKey: ???): Path<T>;
I tried to improve that, and have at least validly typed function parameters that behave correctly:
// Helper to determine valid keys for a nested object value
type NestedObjectKey<
T extends FieldPathValue<TFieldValues, TFieldPath>,
TFieldValues extends FieldValues = FieldValues,
TFieldPath extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = T extends Record<string, any> ? keyof T : never;
// Append
function appendToPath<
TFieldValues extends FieldValues = FieldValues,
TFieldPath extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>(path: TFieldPath, keyToAppend: NestedObjectKey<FieldPathValue<TFieldValues, TFieldPath>>): Path<TFieldValues> {
return `${path}.${keyToAppend}`; // this still raises an error
}
So from a human standpoint this should be enough logically and works as far using the function and its function signature is concerned. However unfortunately TS cannot infer this information as a human can when assembling the path and i have not found a way to do so so far. Last resort would be a custom type assertion, but that would add unnecessary code.
Another way to look at it from a human standpoint is a ${Path<A>}.${Path<B>}
is still a Path<A>
variable if FieldValue<A, Path<A>> = B
. I don't know if that could be translated to a working typescript implementation