#Need help with generic types for factory pattern like method

53 messages · Page 1 of 1 (latest)

ancient maple
#

I am trying to implement a method, that takes a template identifier and the respective template parameters and then returns the rendered email template.
However I cannot get the generic types to work so that Typescript doesn't throw an error.
No matter what I try I always get

Type '{ email: string; } | { someOtherProp: number; newEmail: string; }' is not assignable to type 'IntrinsicAttributes & { email: string; } & { someOtherProp: number; newEmail: string; }'.
  Type '{ email: string; }' is missing the following properties from type '{ someOtherProp: number; newEmail: string; }': someOtherProp, newEmail

Any help would be greatly appreciated!
Is there a way to do this with Typescript?

I have a code example below:

#

Need help with generic types for factory pattern like method

dry trellisBOT
#
astrogd#0

Preview:```ts
...
function renderEmail<T extends Template>(
template: T,
data: Parameters<
typeof EmailTemplates[T]["template"]

[0]
): React.JSX.Element {
const {template: EmailTemplate} =
EmailTemplates[template]
return <EmailTemplate {...data} />
}

// Example usage
const emailData = {email: "example@example.com"}
const renderedEmail = renderEmail(
Template.CONFIRM_EMAIL,
emailData
)
...```

untold violet
#

@ancient maple
Your template might be a ConfirmEmailTemplate or ReportEmailChangeTemplate

#

So typescript is saying that data must be able to satisfy both, which isn't possible.

#

So to make it work we'd need some stranger connections between the types to tell TS that they will match up

#

Hmm I am a bit stumped with how to solve that today.

#

In that situation, the simple answer is just to change the last line to

const renderedEmail = <ConfirmEmailTemplate {...emailData} />;
#

So if you explain what you're trying to do, we can see if there is a suitable workaround, without battling against the type system.

distant jolt
#

@ancient maple Yeah, this looks like a correspondence problem

#

!:corre

dry trellisBOT
#
retsam19#0
`!retsam19:correspondence-problem`:

There's a particular pattern that is safe but hard for the Typescript compiler to handle, which I call the "correspondence problem":

const functionsWithArguments = [
  { func: (arg: string) => {}, arg: "foo" },
  { func: (arg: number) => {}, arg: 0 },
];

for (const { func, arg } of functionsWithArguments) {
  func(arg);
//     ^^^
// Argument of type 'string | number' is not assignable to parameter of type 'never'.
//   Type 'string' is not assignable to type 'never'.
}

The problem is that func is typed as (x: string) => void | (x: number) => void and arg is string | number, but the compiler can't prove that they "correspond": that, for example, arg is only a string when func accepts strings.

As far as the type are concerned, arg could be number, and func could be (arg: string) => void, and that would be a type-error. It's easy for us to see that that won't happen, but that requires understanding the program at a higher-level than the level the compiler operates.

Depending on the specifics there's sometimes clever fixes, but usually I recommend using a type assertion and ignoring the issue:

func(arg as never);
ancient maple
# untold violet So if you explain what you're trying to do, we can see if there is a suitable wo...

In my use case I have a lot more templates. I want to build a service for sending emails from an application I am developing.
I get the template type and some data, want to parse the data for validity using zod and then generate the email content based on the template type.

I'm not a huge fan of using large switch statements for that so I though this might be something to achieve with generics and a "factory"-function that chooses the correct template and zod schema to use.

ancient maple
untold violet
#

If the template was an argument instead of being looked up, it could work

#

so you'd be using renderEmail(templates[Template.CONFIRM_EMAIL], emailData);

#

I don't really have any good ideas here. I would just make it work, using casts if necessary, if you can assure safety by logic.

ancient maple
#

Uuuuh thats a nice idea, I'll try that one out, thanks!
But I think it could still be an issue ensuring type safety on the emailData as it depends on what is being returned by the templates[Template.CONFIRM_EMAIL] so there still needs to be some kind of generic type being transferred

untold violet
#
function makeEmailRenderer<
  Schema extends Zod.ZodType,
  Template extends (props: Schema['_output']) => React.ReactNode
>(schema: Schema, template: Template) {
  return function emailRenderer(data: Schema['_input']) {
    const result = schema.parse(data)
    return <template {...result} />
  }
}

const renderConfirmEmail = makeEmailRenderer(ConfirmEmailSchema, ConfirmEmailTemplate)
const renderReportEmailChange = makeEmailRenderer(ReportEmailChangeSchema, ReportEmailChangeTemplate)

const renderedEmail = renderConfirmEmail(emailData);
#

something like that maybe

dry trellisBOT
#
sandiford#0

Preview:```ts
import React from "react"
import * as zod from "zod"

const ConfirmEmailTemplate = (props: {
email: string
}) => <div>Confirm Email {props.email}</div>
const ReportEmailChangeTemplate = (props: {
someOtherProp: number
newEmail: string
}) => (
<div>
Report Email Change{props.someOtherProp + 1}{" "}
{props.newEmail
...```

untold violet
#
const renderEmail = {
  confirmEmail: makeEmailRenderer(ConfirmEmailSchema, ConfirmEmailTemplate),
  reportEmailChange: makeEmailRenderer(ReportEmailChangeSchema, ReportEmailChangeTemplate)
}
const renderedEmail = renderEmail.confirmEmail(emailData);
#

alternative organisation

#

@ancient maple

#

There is a certain way of structuring code that is TS friendly... looking things up from objects or arrays is not really it

#

I don't know how to explain it, but you want a flow from top to bottom

#

Rather than sort of defining an object, calling a function, reaching back to that object defined earlier, etc.

ancient maple
#

!resolved

#

!rep 353994327103635466

#

!rep 134066166095151105

#

(I hope this is how it works with the reps 😅 )

untold violet
#

!rep @ancient maple

#

I don't know anything about typedocs 🙂

ancient maple
untold violet
#

ah yeah

#

There's probably a way, I just don't know what is. I would think you can mark object properties as being functions.

ancient maple
#

Yeah I have it like this:

function makeEmailRenderer(...) {
/**
   * Renders the given data with the template
   * @param data The data to render
   * @returns The rendered email
   * @throws If the data is invalid
   */
  function emailRenderer(data: Schema["_input"]) {
    const result = schema.parse(data);
    return renderTemplate(template(result));
  }

  return emailRenderer;
}

//Hovering over templateRenderer doesn't show the JSDoc
const templateRenderer = makeEmailRenderer(...);

But the JSDoc will not be visible on the outside :/

untold violet
#

Yeah put it on the object not the function

ancient maple
#
export const renderers = {
  /**
   * Renders the confirm email template.
   * @param data The data to render
   * @returns The rendered email
   * @throws If the data is invalid
   */
  [Template.CONFIRM_EMAIL]: makeTemplateRenderer(
    ConfirmEmailSchema,
    ConfirmEmailTemplate
  ),
}

This also doesn't work sadly

untold violet
#

I suspect that there is a way to make it work

#

But you might need a different syntax

ancient maple
#

Ooooh it works if I remove the return type from makeEmailRenderer

#

Strange

untold violet
#

Really

#

I tried googling and didn't get clear results

ancient maple
#

Yeah. As soon as I specify the return type, the JSDoc vanishes

#

However the Doc only shows when howering over a call of the function, not the variable I save it to...

//Doesn't show JSDoc
const renderer = renderers[...];
// Shows JSDoc
renderer(...);
untold violet
#

this syntax seems suitable

#

But he is complaining that it doesn't work correctly 😄

#

You could make a helpthread for JSDoc help specifically, just in case anyone knows.

#

I typically don't bother to read help threads with a lot of comments, because I assume they are dealt with