I am looking to create various objects that need to adhere to a predefined type, but each object should have its own unique structure. Additionally, I want the ability to ensure conformance to the predefined type at the point of object creation.
Let's consider the code below, which converts a structure defining APIs into the actual APIs themselves, all statically type-checked:
type ApiTemplate = {
[funcName: string]: {
call: (data: any) => void;
handler: (data: any) => void;
}
};
const apiConfig1 = /* see below */;
const apiConfig2 = /* see below */;
const apiConfig3 = /* see below */;
function toApi<T extends ApiTemplate>(
config: T
): { [n in keyof T]: T[n]["call"] } {
return Object.fromEntries(
Object.entries(config).map(
([funcName, apiDef]) => [funcName, apiDef.call]
)
) as any;
}
const api1 = toApi(apiConfig1);
const api2 = toApi(apiConfig2);
const api3 = toApi(apiConfig3);
api1.func1(2); // statically typed function signature
I aim to define each apiConfig*
object in a separate file and have static type checking available while coding these objects. I also want the compiler to catch any errors within the objects. If I did not need this type checking, I could define APIs like this:
const apiConfig1 = {
func1: {
call: (count: number) => console.log("count", count),
handler: (data: any) => console.log(data),
},
func2: {
call: (name: string) => console.log(name),
handler: (data: any) => console.log("string", data),
},
};
However, if there is an error in constructing the object, such as misspelling 'handler' as 'handle', TypeScript will report the error at the call to toApi()
, which is not ideal.
In my attempts to address this issue, I encountered the following challenges:
const apiConfig1 = (function <T extends ApiTemplate>(): T {
return {
func1: {
call: (count: number) => console.log("count", count),
handler: (data: any) => console.log(data),
},
func2: {
call: (name: string) => console.log(name),
handler: (data: any) => console.log("string", data),
},
};
})();
This resulted in the error message:
Type '{ func1: { call: (count: number) => void; handler: (data: any) => void; };
func2: { call: (name: string) => void; handler: (data: any) => void; }; }'
is not assignable to type 'T'.
'{ func1: { call: (count: number) => void; handler: (data: any) => void; };
func2: { call: (name: string) => void; handler: (data: any) => void; }; }'
is assignable to the constraint of type 'T', but 'T' could be instantiated
with a different subtype of constraint 'Template'.ts(2322)
And when trying this approach:
const apiConfig1 = (function <T>(): T extends ApiTemplate ? T : never {
return {
func1: {
call: (count: number) => console.log("count", count),
handler: (data: any) => console.log(data),
},
func2: {
call: (name: string) => console.log(name),
handler: (data: any) => console.log("string", data),
},
}
});
This produced the error message:
Type '{ func1: { call: (count: number) => void; handler: (data: any) => void; };
func2: { call: (name: string) => void; handler: (data: any) => void; }; }'
is not assignable to type 'T extends ApiTemplate ? T : never'.ts(2322)
Is there a solution in TypeScript to achieve what I intend?
NOTE: The method below removes static type-checking from the generated API by toApi()
because it discards the specific type of each object:
const apiConfig1: ApiTemplate = /* ... */;