typescripttypescript-genericstypescript-template-literals

Getting an error when trying to add a key to an object with a template literal


I am trying to add a key to an object using a template literal in TypeScript. This is being done by a function that takes a generic of the object. I keep getting the following error:

Element implicitly has an 'any' type because expression of type '`${string}Bar`' can't be used to index type 'unknown'.

Here is a stripped down version of what I am trying to do:

type FooDatum = {
  foo: string;
  baz: string;
  fooBar?: string;
}

function addFooBar<Datum>(obj: Datum, dataKey: keyof Datum) {
  obj[`${String(dataKey)}Bar`] = obj[dataKey];

  return obj;
}

addFooBar<FooDatum>({ foo: 'bar', baz: 'string' }, 'foo')

Playground


Solution

  • Even though this solution works it may not be the best one for scaling. We must use a generic parameter for the whole object.

    To ensure that adding a new property to an object will work we will define the type as an object with the initial value of the key and optional property with the key postfixed with Bar. The parameter for the key will have the constraint to be the string key of the object.

    function addFooBar<T extends { [P in `${K}Bar`]?: string } & { [P in K]: string }, K extends keyof T & string>(obj: T, dataKey: K) {}
    

    Unfortunately, directly setting the property with the dynamic key will lose the type of the key and going to be widened to just string. To prevent this we will need to have a separate function that is typed and prevents the narrowing:

    // To ensure the dynamic key is typed correctly
    function record<K extends string, V>(key: K, value: V): { [prop in K]: V };
    function record<K extends string, V>(key: K, value: V) {
      return { [key]: value };
    }
    

    Implementation:

    function addFooBar<T extends { [P in `${K}Bar`]?: string } & { [P in K]: string }, K extends keyof T & string>(obj: T, dataKey: K) {
      return { ...obj, ...record(`${dataKey}Bar`, obj[dataKey]) };
    }
    

    Testing:

    // FooDatum & {
    //     fooBar: string;
    // }
    const a = addFooBar({ foo: 'bar', baz: 'string' } as FooDatum, 'foo') // no error
    

    playground