'TypeScript Generics column.id should be within row[key]
I'm writing a TypeScript Interface for Tables:
interface Column {
id: string;
label: string;
}
interface Data {
[key: string]: string;
}
interface Table {
columns: Column[];
data: Data[];
}
I'd like to restrict the allowed values for Column.id
: Each Column.id
must have a matching Data.key
. (But: Not every Data.key
must have a matching Column.id
)
Examples:
This should be allowed, because every Column.id
has a matching Data.key
.
columns: [
{ id: 'foo', label: 'foo' },
{ id: 'bar', label: 'bar' }
]
data: [
{ foo: '123', bar: '456', baz: '789' }
]
But this should not be allowed, because Data[foo]
doesn't exist.
columns: [
{ id: 'foo', label: 'foo' }
]
data: [
{ bar: '456' }
]
How is it possible to write a Table
interface, which applies these constraints?
Solution 1:[1]
You can't make a concrete type that represents this constraint, but you can use generics along with a helper function to infer a generic type that enforces that constraint.
Let's extend the definitions of Column
, Data
, and Table
to be generic in the string properties we care about:
interface Column<K extends string = string> {
id: K,
label: string;
}
type Data<K extends string = string> = Record<K, string>
interface Table<K extends string = string, L extends K = K> {
columns: Column<L>[];
data: Data<K>[];
}
Note how a valid Table<K, L>
, assuming K
and L
are unions of string literals and that K
is as narrow as it can be, expresses the constraint you want. Since L extends K
, it means that columns['id']
must be a subtype of keyof data
.
The following helper function will do the inference for you:
const asTable = <K extends string, L extends K>(x: Table<K, L>) => x;
Okay, let's see if it works:
// no error
const goodTable = asTable({
columns: [
{ id: 'foo', label: 'foo' },
{ id: 'bar', label: 'bar' }
],
data: [
{ foo: '123', bar: '456', baz: '789' }
]
})
// error ... Type '"foo"' is not assignable to type '"bar"'
const badTable = asTable({
columns: [
{ id: 'foo', label: 'foo' }
]
,
data: [
{ bar: '456' }
]
})
Looks good. Hope that helps!
Solution 2:[2]
Here's another solution, which seems to work.
I'm not sure what are the pros and cons compared to @jcalz version.
Maybe someone can leave a comment and explain the differences :)
interface Column<K extends string> {
id: K;
label: string;
}
interface Props<D extends {[key: string]: string}> {
columns: Array<Column<keyof D & string>>;
data: D[];
}
Solution 3:[3]
Maybe easier base on @jcalz
interface Column<K extends string = string> {
name: K;
friendly_name: string;
type: string;
}
type Data<K extends string = string> = Record<K, any>;
interface UniformedData<K extends string = string> {
columns: Column<K>[];
//rows: Data<ValueOf<Pick<Column<K>, 'name'>>>[];//type ValueOf<T>=T[keyof T];
rows: Data<Column<K>['name']>[];
}
// Runtime type-inferring must on funcs
export const asTable = <K extends string>(x: UniformedData<K>) => x;
const check = asTable({
columns: [{ name: 'c1', friendly_name: 'C1', type: 'string' }],
rows: [{ c2: 'x' }], // error!
});
ยทยทยท
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | jcalz |
Solution 2 | Benjamin M |
Solution 3 | Tearf001 |