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

标签 typescript

这里是一个参数的接口(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 change excludeProperties 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/

相关文章:

typescript - 使用汇总生成 typescript 定义文件

typescript - 在 Typescript 中扩展导出变量的接口(interface)

angular - 上下文菜单在我的 ag-grid 中不起作用

javascript - Visual Studio typescript "Uncaught ReferenceError: exports is not defined at...."

javascript - 如何在JS/TS中实现伪阻塞异步队列?

angular - 错误类型错误 : Cannot read property 'invalid' of undefined angular 4

node.js - 找不到名称 "module"

javascript - ionic 3 选项卡中的 subview 在查看子组件时显示两个标题

javascript - 如何从对象创建模板文字类型以具有键=值?

typescript - 将对象的数组/元组转换为由属性键入的对象的通用类型