In TypeScript, there isn't a direct type that matches the values you want Obj
to accept. Instead, you can create a generic type called Obj<T>
. This type acts as a constraint on T
, ensuring that if T extends Obj<T>
, then T
is valid. You could also define a generic helper function asObj(obj)
which checks if obj
is a valid Obj<T>
for some T
.
To achieve this, you might write:
type Obj<T> = ⋯;
and then something like:
const asObj = <T,>(obj: T & Obj<T>): T => obj;
However, a more realistic approach would be:
const asObj = <T,>(obj: T & Obj<T>): T => obj;
The question now is how to actually define Obj<T>
.
You could define it in a manner similar to this. By testing each property of your targeted Obj
type independently, you can structure it as a mapped type based on single-property checking:
type Obj<T> = { [K in keyof T]: F<T[K]> }
This leads us to the need to define F<>
:
type F<T> =
{ [S in Suffixes as \`f${S}\`]:
(props: never) => void
} & { [S in Suffixes as \`args${S}\`]:
T extends Record<\`f${S}\`, (props: infer P) => void> ? P : never
}
type Suffixes = " | "1" | "2" // <-- you can adjust this based on your needs
This definition ensures that F<T>
validates that T
is a proper Obj
property. It requires all expected f
and args
members, with the ability to modify the number via the Suffixes
union type. The f
, f1
, f2
, functions are designated to receive one argument respectively, and args
, args1
, args2
, represent the valid argument types for their respective f
functions.
Lets put this to the test:
const asObj = <T,>(obj: T & Obj<T>): T => obj;
const demo = asObj({
key1: {
f: (props: { name: string, age: number }) => { }, args: { age: 3, name: "" },
f1: (props: { name: 2 }) => { }, args1: { name: 2 },
f2: (props: unknown) => { }, args2: 2
},
key2: {
f: (props: { x: boolean }) => { }, args: { x: true },
f1: (props: { z: Date }) => { }, args1: { z: new Date() },
f2: (props: number) => { }, args2: "oops" // error
},
});
demo.key1.f(demo.key1.args); // okay
demo.key2.f1(demo.key2.args1); // okay
This code operates correctly by enforcing that each arg
corresponds to an appropriate f
method. Errors are flagged if any mistakes are found. Additionally, the compiler effectively remembers the arguments associated with each method through strong typing.
Access playground link here