1

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}!`
 }
]
Raph117
  • 3,441
  • 7
  • 29
  • 50
  • Instead of pseudocode, please revise the question with actual code (a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example)). Ideally including a link to the same code in the [TypeScript Playground](https://www.typescriptlang.org/play?noUncheckedIndexedAccess=true&target=99&useUnknownInCatchVariables=true&exactOptionalPropertyTypes=true#code/Q) so that we can see which errors you're facing and where you're actually stuck in your program. As the question is currently written, it's difficult to tell what the actual issue is. – jsejcksn Jun 22 '22 at 16:03
  • Also note that [TypeScript does not infer literal types for object members](https://stackoverflow.com/a/65355784/438273), so you'll have to use `as const` on every `rowKey` value in your array in order to use the string literal in a mapping operation. – jsejcksn Jun 22 '22 at 16:03
  • Does [this approach](https://tsplay.dev/mpnxgw) meet your needs? If so I can write up an answer explaining it; if not, what am I missing? – jcalz Jun 22 '22 at 18:09

1 Answers1

2

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

jcalz
  • 264,269
  • 27
  • 359
  • 360