I'd like to construct a bunch of different objects that must each conform to a pre-specified type but which themselves each have a type unique to its specific structure. Moreover, I'd like the ability to enforce conformance to the pre-specified type at the place where the object is constructed.
Consider the following code, which converts a structure that defines APIs into the APIs themselves, each of which is 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 want to define each apiConfig* object in a separate file, and I'd like type static checking available from the editor while coding up the objects. I'd also like the compiler to identify errors within the objects. If I didn't want such type checking, I could just define the APIs as follows:
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 a mistake in the object construction, such as mispelling 'handler' as 'handle', TypeScript reports the error at the call to toApi(), which is not where I want the error.
I tried the following:
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),
},
};
})();
But I get this 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 I tried this:
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),
},
}
});
I get this 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 way to do what I want in TypeScript?
NOTE: The following approach eliminates static type-checking from the API that toApi() generates because it discards each object's unique type:
const apiConfig1: ApiTemplate = /* ... */;
I think you want a simple type annotation:
Changing
handlertohandlefor example shows the error directly where the typo is.