#Enums, Enum Record, and Generic Classes

16 messages · Page 1 of 1 (latest)

devout musk
#

👋 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

steel kayakBOT
#
shuckle#1000

Preview:```ts
import {z} from "zod"

export enum MicroConnectorEndpoint {
HelloWorld = "HELLO_WORLD",
Fail = "FAIL",
}

export const MicroConnectorSchemas = {
[MicroConnectorEndpoint.HelloWorld]: {
input: z.object({}),
output: z.string(),
},
[MicroConnectorEndpoint.Fail]:
...```

devout musk
#

!helper

pallid locust
#

i said this somewhere else too but there is always the function createSend(...) {...}; SomeMicroservice.prototype.send = createSend(...);
afaik, there isn't a good way to go around the copy paste issue when extending a class

devout musk
pallid locust
#

also im pretty sure this shouldn't have keyof

#

unless you are trying to pass in the key itself? which is kinda weird

devout musk
pallid locust
#

yeah then it should just be Endpoints

devout musk
#

Ah I see

devout musk
#

Not sure if it falls into this thread, or if I should open another one; I did mostly get it working it seems, there's just one additional issue that I'm trying to get sorted:

Mostly working code:

import { z } from 'zod';

type Enum<T extends string | number = string | number> = {
  [P in keyof T]: T extends Record<P, infer U> ? U : never;
};

type SomeZodSchema = z.AnyZodObject;

type DataRecord<T extends Enum> = Record<
  T,
  {
    input: SomeZodSchema;
    output: SomeZodSchema;
  }
>;

abstract class BaseService<
  Endpoints extends Enum,
  Schemas extends DataRecord<Endpoints> = DataRecord<Endpoints>,
> {
  constructor(readonly schemas: Schemas) {}

  async send<SpecifiedEndpoint extends Endpoints = Endpoints>(
    endpoint: SpecifiedEndpoint,
    data: unknown,
  ): Promise<z.infer<Schemas[SpecifiedEndpoint]['output']>> {
    const endpointSchema = this.schemas[endpoint];

    const validatedInput = endpointSchema.input.parse(data);

    const validatedOutput = endpointSchema.output.parse(data);

    return validatedOutput;
  }
}

enum MyEndpoints {
  A = 'a',
  B = 'b',
  C = 'c',
}

const MySchemas: DataRecord<MyEndpoints> = {
  [MyEndpoints.A]: {
    input: z.object({ foo: z.number() }),
    output: z.object({ foo: z.number() }),
  },
  [MyEndpoints.B]: {
    input: z.object({ bar: z.number() }),
    output: z.object({ bar: z.number() }),
  },
  [MyEndpoints.C]: {
    input: z.object({ fiz: z.number() }),
    output: z.object({ fiz: z.number() }),
  },
};

class MyService extends BaseService<MyEndpoints> {
  constructor() {
    super(MySchemas);
  }
}

const s = new MyService();

s.send(MyEndpoints.A, {}).then((r) => console.log(r)); // typeof r -> any; should be { foo: number }
s.send(MyEndpoints.B, {}).then((r) => console.log(r)); // typeof r -> any; should be { bar: number }
#

I'm not sure how to narrow the types this way; I feel like the issue might be with the DataRecord type; which only narrows as far as AnyZodObject instead of the specific schemas that are provided. It's just unclear how I can have the return type properly match the looked up value

devout musk
#

Follow up

devout musk
#

I got it figured out; works great, thank you for the help!