'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 |
