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

时间:2018-11-30 02:19:35

标签: typescript

这是参数的界面

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

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,但要排除key中包含的excludeProperties。像这样:

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

上面的声明将导致错误:[ts] Cannot find name 'excludeProperties'. [2304],这是因为TS无法找到excludeProperties的位置。

如何执行此操作? 谢谢

1 个答案:

答案 0 :(得分:0)

是否可能这样做取决于此问题的答案:

  

excludeProperties会在运行时更改吗?还是您希望能够在运行时更改excludeProperties

如果答案为“是”,则不可能这样做,其原因将在后面解释。

让我们首先讨论答案为“否”的解决方案。

如果excludeProperties在运行时将是不可变的,则考虑到其目的是限制titles的类型,它实际上是类型约束而不是值。

  

类型约束与价值(我自己的理解)
  类型约束:说“ titles”的关键字不应该包含“名称”和“年龄” ”。这是规则,是抽象的。不需要包含['name', 'age']的具体变量。
  :值为['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,即多余的属性检查生效,您就可以从excludePropertiestitles中获取详尽的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' }的值,并且类型系统无法改变变量持有的值。虚构的类型在那里崩溃了。

Furthur,如果您要从中获取动态功能,该规则甚至不成立。

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'])是表达编译时确定性排除的另一种方式。据我所知,打字稿中没有办法利用这种“确定性”。

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

实际上,不能通过数组变量告诉编译器类型信息的本质是,JavaScript中无法用文字来表示不可变数组(Object.freeze是函数调用,而不是文字),但是编译器可以利用它。