Svish
Svish

Reputation: 157991

How to express "function with T as parameter | string which is a key of T" in TypeScript

I have the following TypeScript, which does work:

interface Columns {
  [s: string]: string | ((item: any) => string);
}

const exportAsCsv = function (data: any[], columns: Columns): void {
  const header = Object.keys(columns)
    .map(x => `"${x}"`)
    .join(";");

  const rows = [];
  for (const item of data) {
    const row = Object
      .values(columns)
      .map(field => typeof field === 'function' ? field(item) : item[field])
      .map(x => (x || '').replace(/"/, '""'))
      .map(x => `"${x}"`)
      .join(";");
    rows.push(row);
  }
  console.log([header, ...rows].join("\r\n"));
}

The idea is you pass in an array of objects, and a column object where the keys are the headers (can be any string) and the value should be either the name of a property or a function returning a value.

const users = [{id: 1, name: 'Alice', isCool: true}, ...];
const columns = {
  'Id': 'id, 
  'Name': 'name', 
  'Is cool': u => u.isCool ? 'Yes' : 'No',
};
exportToCsv(users, columns);

This all works, but I'd like stricter typing. The following "works", with the exception that I just can't figure out how to write the Columns type generically. Keep getting stuff not being assignable, type parameters being declared but not used, etc, etc.

interface Columns<T> {
  [s: string]: ?;
}
const exportAsCsv = function <T> (data: T[], columns: Columns<T>): void

How do I express this properly?

Upvotes: 1

Views: 112

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249536

You can ensure the value of Columns is either a key of T or a function accepting T using this type:

interface Columns<T> {
    [s: string]: keyof T | ((item: T) => string);
}

const exportAsCsv = function <T>(data: T[], columns: Columns<T>): void {
   //...
}

const users = [{ id: 1, name: 'Alice', isCool: true }];
exportAsCsv(users, {
    'Id': 'id',
    'Name': 'name',
    'Is cool': u => u.isCool ? 'Yes' : 'No',
});
exportAsCsv(users, {
    'Id': 'id',
    'Name': 'name2', // error 
    'Is cool': u => u.isCool ? 'Yes' : 'No', //error
});

You can also create the columns separately from the call but you need to specify T:

const users = [{ id: 1, name: 'Alice', isCool: true }];
const columns : Columns<typeof users[number]> = {
    'Id': 'id',
    'Name': 'name',
    'Is cool': u => u.isCool ? 'Yes' : 'No',
};
exportAsCsv(users, columns);

Upvotes: 1

Related Questions