如何用同一对象中的另一个属性约束一个属性的类型



这是参数的接口

它是一个具有以下签名的对象:

interface IRenderDataToTable<T> {
data: T[];
titles: { [key in keyof T]: string };
excludeProperties: (keyof T)[];
}

示例数据:

  • data是这样的:{id: 1, name: 'a', age: 2}
  • excludeProperties将为['id']
  • 因此,titles应该是{name: 'User Name', age: 'User Age'}
  • 因为id已被排除

问题

类型检查大多有效,只有一个问题,

titles: { [key in keyof T]: string };
excludeProperties: (keyof T)[];

我希望titles包含keyof T,但不包括excludeProperties中包含的key。类似这样的东西:

titles: { [key in keyof Pick<T, Exclude<keyof T, excludeProperties>>]: string };

上面的声明将导致一个错误:[ts] Cannot find name 'excludeProperties'. [2304],这是因为TS找不到excludeProperties在哪里。

如何做到这一点?感谢

是否可以这样做取决于这个问题的答案:

excludeProperties在运行时会发生更改吗?或者您希望能够在运行时更改excludeProperties吗?

如果答案是"是",则不可能这样做,原因将在稍后解释。

当答案是"否"时,让我们首先讨论解决方案。

如果excludeProperties在运行时是不可变的,考虑到它的目的是限制titles的类型,它更像是一个类型约束,而不是一个值。

类型约束与值(我自己的理解(
Type constraint:说">titles不应该有'name'和'age'的键"。这是一条规则,而且是抽象的。不需要有一个包含CCD_ 19的具体变量
:具体变量,值为['name', 'age']。它存在于计算机内存中并且是具体的。你可以对它进行迭代,向它推送新元素,甚至从中弹出元素。如果你不需要迭代,就不需要推送或弹出。

因此我们可以将类型声明为

interface Foo<T, E extends keyof T> {
data: T[],
titles: { [key in Exclude<keyof T, E>]: string }
}

用法:

interface Data {
id: number,
name: string,
age: number
}

let foo: Foo<Data, 'name' | 'age'>
foo = {
data: [{ id: 1, name: 'Alice', age: 20 }],
titles: {
id: 'Title for id'
}
}
foo = {
data: [{ id: 1, name: 'Alice', age: 20 }],
titles: {
id: 'Title for id',
name: 'Title for name'
// error: Type '{ id: string; name: string; }' is not assignable to type '{ id: string; }'.
}
}
const titles = {
id: 'Title for id',
name: 'Title for name'
}
foo = {
data: [{ id: 1, name: 'Alice', age: 20 }],
titles: titles
// can not catch this since "Excess Property Checks" does not work in assignments
// see: "http://www.typescriptlang.org/docs/handbook/interfaces.html#excess-property-checks
}

如果您确实需要excludeProperties作为变量而不是类型约束(即,您需要它的具体值来迭代或做其他事情(,您可以像一样将其添加回来

interface Foo<T, E extends keyof T> {
data: T[],
titles: { [key in Exclude<keyof T, E>]: string },
excludeProperties: E[]
}

但这并不能保证excludeProperties的详尽性。

let foo: Foo<Data, 'name' | 'age'> = {
data: [{ id: 1, name: 'Alice', age: 20 }],
titles: {
id: 'Title for id'
},
excludePropreties: ['name'] // is valid from the type checking perspective though 'age' is missing
}

实际上,只要titles是使用对象文字提供的,即Excess Property Checks生效,就可以从titles:const excludeProperties = Object.keys(foo.titles)中检索穷举的excludeProperties

尽管如此,如果通过使用另一个变量来提供titles,则不能保证穷尽性。


解释为什么无法在运行时更改excludeProperties

假设我们确实有一个满足需求的类型。然后一个像这样的变量向它证实:

const foo = {
data: { id: 1, age: 2, name: 'Alice' },
titles: { id: 'ID', age: 'Age' },
excludeProperties: ['name']
}

然后如果我修改excludeProperties

foo.excludeProperties.push('age')

为了实现预期的约束,foo.titles的类型应该从{ id: string, age: string }更改为{ id: string }。但是,foo.titles的值确实为{ id: 'ID', age: 'Age' },类型系统不可能更改变量的值。想象中的类型在那里崩塌。

此外,如果你想从中获得动态功能,这个规则甚至不成立

const foo = {
data: { id: 1, age: 2, name: 'Alice' },
titles: { id: 'ID' },
excludeProperties: ['name', 'age']
}

那么,如果我运行

foo.excludeProperties.pop()

则CCD_ 34不可能在空中为密钥CCD_。


更新:解释动态"excludeProperties"不可能的另一种方式

编译器无法在编译时预先知道excludeProperties将包含什么,因为它将在运行时分配。因此,它不知道要排除什么,titles的类型应该是什么

因此,为了使编译器能够确定要排除的内容,我们需要在编译时使excludeProperties具有确定性。我上面展示的解决方案通过在编译时确定的泛型类型参数表达这种排除思想来实现这一点。

可以说Object.freeze(['id', 'name'])是表示编译时确定性排除的另一种方式。据我所知,打字稿中没有办法利用这种"确定性"。

我的直觉告诉我这是真的。静态类型系统应仅从静态分析中收集其关于代码的所有知识,即它不应理解和假设某个函数的行为(在这种情况下为Object.freeze(。只要类型系统对Object.freeze的实际行为是不可知的,Object.freeze(['id', 'name'])就不是编译时的决定性表达式。

事实上,不能通过数组变量告诉编译器类型信息的本质是,javascript中没有办法用文字(Object.freeze是一个函数调用,而不是文字(来表达不可变的数组,然而,编译器可以利用它。

最新更新