I want to extend the function argument type by adding a function with any name to each object and return it, but i am facing a problem with naming the key of the added function
export type Expand<T> = {
[K in keyof T as K]: T[K] extends { [K in string]: unknown }
? ExpandData<T[K]>
: T[K] | ((p: T) => T);
};
type ExpandData<T extends { [K in string]: unknown }> = T | Expand<T>;
export const create = <T,>(params: Expand<T>): Expand<T> => {
return params;
};
I want to use it like this, where the function change: (val) => val will return me the value of the object where it is located ( with any name instead of "change" )
type ICreate = {
param: {
first: {
value: number;
}
}
}
const result = create<ICreate>({param: {
first: {
value: 1,
change: (val)=> val, // type val = { value: number }
}
}})
and get as a result type in the form
{
param: {
first: {
value: number
change: Function
}
}
}
How to do this correctly? ( ICreate type may change, I'm looking for a generic solution )
There really isn't a useful specific type that works this way. You'd like to say that you'll allow T
to have any extra key not present in T
, and the property at that key will be of type (val: T) => T
. The stumbling block is that you can't say "any key except for keyof T
". That would require something like a "rest index signature" or a "default property type" as requested in microsoft/TypeScript#17867. But that isn't part of the language. There are various workarounds discussed at How to define Typescript type as a dictionary of strings but with one numeric "id" property. You could certainly describe a generic constraint which validates objects like this, but it won't let you infer the parameter type of the functions you add. It's a mess to do it that way.
It would be much much better for you to add a single property with a known key (like fns
) that contains values of type (val: T) => T
... pushing those functions down one level. That can be described as a specific type like this:
type Expand<T> =
T extends object ? {
[K in keyof T]: Expand<T[K]> } &
{ fns?: { [k: string]: (val: T) => T } } :
T
It's a recursive conditional type that intersects each object type with an object optionally containing a fns
property with an index signature. For
type ICreate = {
param: {
first: {
value: number;
}
}
}
the expanded version is equivalent to
type ExpandICreate = Expand<ICreate>;
/* type ExpandICreate = {
param: {
first: {
value: number;
fns?: { [k: string]: (val: { value: number }) => { value: number }}
}
fns?: { [k: string]:
(val: { first: { value: number }}) => { first: { value: number }}
}
}
fns?: { [k: string]: (val: ICreate) => ICreate }
} */
That's enough to make your original version sort of work, but an index signature doesn't "remember" which keys are present, and optional properties don't "remember" if they are there or not:
const create = <T,>(params: Expand<T>) => params;
const result = create<ICreate>({
param: {
first: {
value: 1,
fns: {
change: (val) => val,
}
}
}
});
result.param.first.value.toFixed(); // okay
result.param.first.fns.change({ value: 1 }); // error, fns might be undefined
result.param.first.fns?.change({ value: 1 }); // okay
result.param.first.fns?.whaaaaa({ value: 1 }); // also okay
If you want TS to know that fns
exists under first
and that change
exists under fns
and that whaaaaaa
does not exist under fns
, then you need to make create()
even more generic. You'd like to write
const create = <T, U extends Expand<T>>(params: U) => params;
but there's no way to call this where you manually specify T
and have the compiler infer U
. That would be microsoft/TypeScript#26242 and it isn't part of the language. The workaround here is currying:
const create = <T,>() => <U extends Expand<T>>(params: U) => params;
So you call create<ICreate>()
and that returns a function you call with params
:
const result = create<ICreate>()({
param: {
first: {
value: 1,
fns: {
change: (val) => val,
}
}
}
})
result.param.first.value.toFixed(); // okay
result.param.first.fns.change({ value: 1 }); // okay
result.param.first.fns.whaaaaa({ value: 1 }); // error
That now works how you'd like it.