TypeScript 泛型 column.id 应该在行 [key] 内

TypeScript Generics column.id should be within row[key]

我正在为表编写 TypeScript 接口:

interface Column {
    id: string;
    label: string;
}

interface Data {
    [key: string]: string;
}

interface Table {
    columns: Column[];
    data: Data[];
}

我想限制 Column.id 的允许值:每个 Column.id 必须有一个匹配的 Data.key。 (但是:不是每个Data.key必须有匹配的Column.id

示例:

这应该是允许的,因为每个Column.id都有匹配的Data.key

columns: [
  { id: 'foo', label: 'foo' },
  { id: 'bar', label: 'bar' }
]

data: [
  { foo: '123', bar: '456', baz: '789' }
]

但这应该不允许,因为Data[foo]不存在。

columns: [
  { id: 'foo', label: 'foo' }
]

data: [
  { bar: '456' }
]

如何编写应用这些约束的 Table 接口?

您不能创建表示该约束的具体类型,但您可以使用generics和辅助函数来推断强制执行该约束的泛型类型约束。

让我们扩展 ColumnDataTable 的定义,使其在我们关心的字符串属性中通用:

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>[];
}

注意 Table<K, L> 是如何有效的,假设 KLstring literals 的并集并且 K 尽可能窄,表达你想要的约束。由于 L extends K,这意味着 columns['id'] 必须是 keyof data 的子类型。

以下辅助函数将为您进行推理:

const asTable = <K extends string, L extends K>(x: Table<K, L>) => x;

好的,让我们看看它是否有效:

// 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' }
  ]
})

看起来不错。希望对您有所帮助!

这是另一个似乎有效的解决方案。

我不确定与@jcalz 版本相比有哪些优缺点。

也许有人可以发表评论并解释差异:)

interface Column<K extends string> {
    id: K;
    label: string;
}

interface Props<D extends {[key: string]: string}> {
    columns: Array<Column<keyof D & string>>;
    data: D[];
}

基于@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!
});
···