Raph117
Raph117

Reputation: 3821

Typescript - defining table columns (mapped types?)

I'm not totally sure how to do this typing:

export interface EnrichedTableColumn<T> {
    title: string;
    rowKey: keyof T;
    formatRow?: RowFormatter<The type defined in T for the key that is the rowKey here>; <- don't know how to do this 
}

I want to say that the rowKey will be a key of interface T. So let's say T will be

interface TImplementation {
    hello: string;
    goodbye: number;
}

I would want rowKey to have to be either "hello" or "goodbye". likewise, for formatRow, I would like to pass the type of that same key-value pair, i.e in the case of hello, formatRow would be RowFormatter<string>.

I think I can do this with mapped types, but I'm not sure. Any help would be appreciated, I can clarify stuff if this is a bit confusing.

The use case is I would like to pass an array of these columns to a table, specifying only the interface which will define the data in the table.

so in the example, the table would have data that looks like this:

{
   hello: string;
   goodbye: number;
}[]

TS could then tell me if I make a mistake, for example, passing the following:

const COLS = [
 {
   title: "Hello",
   rowKey: "hello",
   formatRow: (value: number <- this is the mistake, should be string) => `Hello, ${value}!`
 }
]

Upvotes: 1

Views: 1877

Answers (1)

jcalz
jcalz

Reputation: 328059

You really want EnrichedTableColumn<TImplementation> to be a union like

type Test = EnrichedTableColumn<TImplementation>
/* type Test = {
    title: string;
    rowKey: "hello";
    formatRow?: ((value: string) => string) | undefined;
} | {
    title: string;
    rowKey: "goodbye";
    formatRow?: ((value: number) => string) | undefined;
} */

where it is either an object with a "hello" rowKey property whose formatRow method (if present) accepts a string argument, or an object with a "goodbye" rowKey property whose formatRow method (if present) accepts a number object.

That will allow the compiler to reason about the formatRow parameter based on the rowKey value, as shown here:

const COLS: EnrichedTableColumn<TImplementation>[] = [
  {
    title: "Hello",
    rowKey: "hello",
    formatRow: (value) => `Hello, ${value.toUpperCase()}!`
  },
  {
    title: "Goodbye",
    rowKey: "goodbye",
    formatRow: (value) => `Hello, ${value.toUpperCase()}!` // error! toUpperCase does not exist on number
  },

]

So, how can we write EnrichedTableColumn<T>? Well, it can't be an interface, which are single object types and not unions of them. But that probably doesn't matter; instead it can be a type function, like this:

type EnrichedTableColumn<T extends object> = { [K in keyof T]:
  { title: string; rowKey: K, formatRow?: (value: T[K]) => string }
}[keyof T]

This is a distributive object type (as coined in microsoft/TypeScript#47109) of the form {[K in X]: F<K>}[X], where X is a keylike type. It's a mapped type over a set of keys X, into which you immediately indexed with the same set of keys. If X is a union like "hello" | "goodbye", then the resulting type is F<"hello"> | F<"goodbye">.

In the above definition, X is keyof T, and the type function F<K> is { title: string; rowKey: K, formatRow?: (value: T[K]) => string }. Note how the value parameter of formatRow is of type T[K], which is the type you get if you index into a value of type T with a value of type K.

You can verify that this produces the desired type for EnrichedTableColumn<TImplementation>, and the desired behavior for COLS, as shown above.

Playground link to code

Upvotes: 2

Related Questions