Simcha
Simcha

Reputation: 3400

Dynamic keys in Typescript

I have some formData object like this (I'm getting it in runtime):

const formData = {
  xAxisColumn: 'abc',
  periodColumn: 'cde'
};

And data that I'm getting from the server that has the next structure:

const response = {
  abc: 'some data 1',
  cde: 123,
  otherColumn: 'bla...'
}

I'm trying in Typescript to create a type for the response object where the keys are dynamic and based on formData values.

Something like:

type TResponse<T, U> = {
 [T]: string;
 [U]: number;
}

But this is incorrect for TS, so how can I do it? Thanks.

Upvotes: 1

Views: 122

Answers (1)

jcalz
jcalz

Reputation: 330086

Without knowing exactly how the rest of your code would make use of it, the type you're looking for could be expressed like this:

type TResponse<KS extends PropertyKey, KN extends PropertyKey> = 
  Record<KS, string> & Record<KN, number>;

that's an intersection of two Record types. It says that TResponse<KS, KN> has a string property at the key of type KS, and a number property at the key of type KN.


You could possibly then use it like this:

const formData = {
  xAxisColumn: 'abc',
  periodColumn: 'cde'
} as const;

const response = {
  abc: 'some data 1',
  cde: 123,
  otherColumn: 'bla...'
}

function processResponse<KS extends PropertyKey, KN extends PropertyKey>(
  formData: { xAxisColumn: KS, periodColumn: KN },
  response: TResponse<KS, KN>
) {
  const xAxis: string = response[formData.xAxisColumn];
  const period: number = response[formData.periodColumn];
  // ... process them here
}

processResponse(formData, response); // okay
processResponse({ xAxisColumn: "x", periodColumn: "p" }, { x: "okay", p: 123 }); // also okay
processResponse({ xAxisColumn: "x", periodColumn: "p" }, { x: 123, p: 456 }); // error!
// number is not assignable to string -------------------> ~

Or maybe

function keyVal<K extends PropertyKey, V>(key: K, val: V) {
  return { [key]: val } as Record<K, V>;
}

function makeResponse<KS extends PropertyKey, KN extends PropertyKey>(
  formData: { xAxisColumn: KS, periodColumn: KN }
): TResponse<KS, KN> {
  return Object.assign(keyVal(formData.xAxisColumn, "x axis data"), keyVal(formData.periodColumn, 123));
}

The point of the example code is to show that the compiler will enforce the constraint that response is of the right shape given the values of formData... as long as the compiler knows the literal values of formData. You will find TResponse<KS, KN> much easier to use when KS and KN are known literal types.

Inside the implementation of processResponse() and makeResponse(), where KS and KN are unspecified generic types, it becomes more difficult for the compiler to reason about TResponse<KS, KN> and you need to find roundabout ways to express manipulating properties of unspecified keys, possibly involving type assertions as in keyVal() above.

So while you can express TResponse, it might not be worth it to do so.

Anyway, hope that helps; good luck!

Playground link to code

Upvotes: 1

Related Questions