👋 Hey everybody; I've been struggling with this typing issue for a few days now, and I keep running into a few walls.
The goal is to have a generic class that can accept an enum; and some Record<valueof SomeEnum, SomeValue>, and then have some function that accepts SomeEnum.key and can lookup on that record with type safety. I've been banging my head against the wall.
Here's an example of what I would like it to do
type EndpointSchemas = {
input: someZodSchema,
output: someZodSchema
}
// Declare endpoint patterns
enum SomeServiceEndpointEnum {
EndpointName = "EndpointPattern"
}
// Declare endpoint schemas
const SomeServiceEndpointSchemas: SomeFancyRecordType<SomeSerivceEndpointEnum, EndpointSchemas> = {
[SomeServiceEndpointEnum.EndpointName]: {
input: someZodSchema
output: someZodSchema
}
}
// Declare some generic class
abstract class AbstractServiceConnector<
Endpoints extends SomeEnum,
Schemas = SomeFancyRecordType<Endpoints, EndpointSchemas>
> {
constructor(
private readonly schemas: Schemas
) {}
async send<
Endpoint extends keyof Endpoints
SchemaType extends Schemas[Endpoint] // Not indexable
>(
endpoint: Endpoint,
data: unknown
): Promise<z.infer<typeof Schemas[Endpoint]["output"]>> {
// This fails
const {input, output} = this.schemas[endpoint]
}
}
// Provide endpoint enum as a generic
class SomeServiceConnector extends BaseServiceConnector<SomeServiceEndpointEnum> {
constructor() {
// Provide schema mapping as a value
super(SomeServiceEndpointSchemas)
}
}
// Note: this is in a NestJS app, so DI makes this pattern make a bit more sense.
// I am open to alternatives as long as the usage would remain about the same.
const someServiceConnector = new SomeServiceConnector()
someServiceConnetor.send(
SomeServiceEndpointEnum.EndpointName,
{ some: "Data to be validated" }
) // -> Promise<{some: "Data that has been returned"}>
I have had this working (sorta), by not using an abstract class here; example on Typescript Playground. The biggest issue here is that it results in a lot of copy-paste code; because the actual logic inside the class remains completely identical, there are just a few generics that have to change