Geza Kerecsenyi
Geza Kerecsenyi

Reputation: 1218

Concatenated string as key of object in Typescript?

Suppose I have the following JS code:

export const giveMeFunctions = (namePrefix, number) => {
    const functionA = (x) => number + x;
    const functionB = (x, y) => number * x - y;
    
    return {
        [`${namePrefix}Add`]: functionA,
        [`${namePrefix}MultSub`]: functionB,
    };
}

// This is handy because I can then do:
const {piAdd, piMultSub} = giveMeFunctions('pi', Math.PI);
const {eAdd, eMultSub} = giveMeFunctions('e', Math.E);
// No confusion between variable names, and object interpolation makes it easy to use

I cannot, however, think of a good way to type the return value of this function in Typescript. As you can see, the two keys have different types, so I can't simply do something like

const giveMeFunctions<N extends string> = (number: number): {
    [key: string]: Function;
} => {...}

as this would allow accidental interchanging of the arguments of functionA and functionB. It seems logical to me that this should be possible with Typescript, as it's all possible at compile-time. For instance, if we simply took out the concatenation step, this would be easily possible:

export const giveMeFunctions<T extends string> = (number: number): Record<T, string> => {...}

Something I expected to work would be:

const giveMeFunctions<N extends string> = (number: number): {
    [N + 'Add']: string,
    [N + 'MultSub']: string,
} => {...}

or even:

const giveMeFunctions<N extends string> = (namePrefix: N, number: number): {
    [N + 'Add']: string,
    [N + 'MultSub']: string,
} => {...}

but both of these complain that:

TS1170: A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.

Is there any way to achieve the desired effect of an object type with keys defined by a concatenation of a user-specified string and a hard-coded suffix?

Upvotes: 1

Views: 3327

Answers (2)

You can do smth like that:

const record = <
  Prop extends PropertyKey,
  Value extends (...args: any[]) => any
>(prop: Prop, value: Value) =>
  ({ [prop]: value }) as Record<Prop, Value>

export const giveMeFunctions = <
  T extends string,
  N extends number
>(namePrefix: T, number: N) =>
({
  ...record(`${namePrefix}Add`, (x: N) => number + x,),
  ...record(`${namePrefix}MultSub`, (x: N, y: N) => number * x - y),
})

const result = giveMeFunctions('pi', Math.PI);
result.piAdd // (x: number) => number
result.piMultSub // (x: number, y: number) => number

Playground

record function does the trick. Object with computed proeprties like {[prop]:key} is resolved as {[props:string]:any type} by the default.

As you might have noticed, I used type casting as Record<Prop, Value> to avoid this behavior

Upvotes: 2

ZloiGoroh
ZloiGoroh

Reputation: 431

If i understood correctly, you want to do something like this

type keyAdd = `${string}Add`
type keyMultSub = `${string}MultSub`

Guess it should work

const giveMeFunctions = (number: number): {
    [keyAdd]: Function,
    [keyMultSub]: Function
} => {/*some function*/}

Upvotes: 1

Related Questions