这里是一个参数的接口(interface)
它是一个具有以下签名的对象:
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] 找不到名称“excludeProperties”。 [2304]
这是因为 TS 找不到 excludeProperties
的位置。
如何做到这一点? 谢谢
最佳答案
是否可以这样做取决于这个问题的答案:
Would
excludeProperties
change in runtime? or Do you expect to be able to changeexcludeProperties
in runtime?
如果答案是"is",则不可能这样做,原因稍后解释。
让我们先讨论当答案为“否”时的解决方案。
如果excludeProperties
在运行时将是不可变的,考虑到它的目的是限制 titles
的类型,它更像是一个类型约束而不是一个值。
Type constraint vs value (my own understanding)
type constraint: Saying of "titles
should not have keys of 'name' and 'age'". It's a rule and is abstract. There is no need to have a concrete variable which holds['name', 'age']
.
value: Concrete variable with value of['name', 'age']
. It exists in the computer memory and is concrete. You can iterate over it, push new elements to it and even pop elements from it. While you do not need it if you do not need to iterate, push or pop.
所以我们可以将类型声明为
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
使用对象文字提供,即多余的属性检查生效,您可以检索详尽的 excludeProperties
来自 titles
: const excludeProperties = Object.keys(foo.titles)
.
不过,如果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 }
来自 { id: string, age: 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()
foo.titles
是不可能的为键 age
补一个值从空中。
更新:另一种解释动态“excludeProperties”不可能性的方法
编译器不知道什么excludeProperties
将在编译时提前保存,因为它将在运行时分配。因此它不知道要排除什么,应该排除什么titles
的类型是。
因此,为了使编译器能够确定要排除的内容,我们需要制作 excludeProperties
在编译时确定。我上面展示的解决方案通过在编译时确定的泛型类型参数表达这种排除思想来实现这一点。
有人可能会说 Object.freeze(['id', 'name'])
是表达编译时确定排除的另一种方式。据我所知, typescript 无法利用这种“确定性”。
我的直觉告诉我这是真的。静态类型系统应仅从静态分析中收集有关代码的所有知识,即它不应理解和假设某个函数的行为(在本例中为 Object.freeze
)。只要类型系统不知道 Object.freeze
的实际行为,它应该是,Object.freeze(['id', 'name'])
不是编译时确定的表达式。
事实上,不能通过数组变量告诉编译器类型信息的本质是在 javascript 中没有办法用文字来表达不可变数组(Object.freeze 是函数调用,而不是literal),然而,它可以被编译器利用。
关于typescript - 如何用同一对象中的另一个属性约束一个属性的类型?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53550309/