Reputation: 3821
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
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.
Upvotes: 2