Home > Enterprise >  Typescript template literal as generic parameter
Typescript template literal as generic parameter

Time:09-21

I have an API that returns an object in the form similar to this:

{
  foo: number,
  foo_comment: string,
  foo_formatted_value: string,
  _foo_created_by: string
  bar: number,
  bar_comment: string,
  bar_formatted_value: string,
  _bar_created_by: string
}

Looking at this answer, it looks like I can make a generic that will add a prefix to the keys of an object. Is there a way to have a generic that accepts a template literal parameter in like manner?

I am thinking something like this:

type Data = {
  foo: number,
  bar: number
}

type Template<T, TemplateFunction extends TemplateLiteralFunction,  V = void> = {
  [K in keyof T as K extends string ? TemplateFunction(K) : never]: (V extends void? T[K]: V)
}

type DataAndMetaData = 
      Data &
      Template<Data,`${1}_comment`>&
      Template<Data,`${1}_formatted_value`>&
      Template<Data,`_${1}_created_by`>;

Of course, the ${1} is the part that doesn't work. Perhaps if there were type-generating functions: (Key:string)=>'${Key}...'

CodePudding user response:

You can't abstract over template literal types in this way, but you don't need to. Instead you can take plain string literal types and use template literal type inference to substitute your "${1}" string with the keys you want.

It could look like this:

type Template<T, S extends string, V = void, I extends string = "${1}"> = {
    [K in keyof T as K extends string ?
    S extends `${infer F}${I}${infer R}` ? `${F}${K}${R}` : K :
    K]: V extends void ? T[K] : V;
}

And we can use it like this:

type DataAndMetaData =
    Data &
    Template<Data, "${1}_comment" | "${1}_formatted_value" | "_${1}_created_by", string>

/* type DataAndMetaData = Data & {
    foo_comment: string;
    foo_formatted_value: string;
    _foo_created_by: string;
    bar_comment: string;
    bar_formatted_value: string;
    _bar_created_by: string;
}*/

Note that you could write Data & Template<Data, "${1}_comment", string> & Template<Data, "${1}_formatted_value", string> & Template<Data, "_${1}_created_by", string>, but it's cleaner to use the distributive behavior to get the desired output type all at once (or nearly so... still need that initial Data & in there).

Also, in the above, the "${1}" value is essentially arbitrary, since we're not really using template literal interpolation. Maybe you want to use a different symbol to represent the thing to be replaced. I added another type parameter, I, to represent this value, and made it default to "${1}". So you might want to change the default, or even set it explicitly when you use Template:

type Also = Template<Data, "*" | "*_xyz", string, "*">
/* type Also = {
    foo: string;
    foo_xyz: string;
    bar: string;
    bar_xyz: string;
} */

Also note that the definition above only does a single substitution and will behave strangely if you include multiple instances of the sigil:

type Hmm = Template<Data, "${1}_or_not_${1}_that_is_the_question">;
/* type Hmm = {
    "foo_or_not_${1}_that_is_the_question": number;
    "bar_or_not_${1}_that_is_the_question": number;
} */

I'm assuming that doesn't matter, but if it does, one could write a recursive version of the substitution logic to deal with it—in another question, since it's out of scope for this one.

  • Related