【问题标题】:Typescript recursive type with indexer带有索引器的打字稿递归类型
【发布时间】:2017-09-07 12:54:11
【问题描述】:

我想定义一个自身递归的类型,本质上是这样的:

interface CSSProperties {
  marginLeft?: string | number
  [key: string]?: CSSProperties
}

不幸的是,typescript docs 说:

虽然字符串索引签名是描述“字典”模式的有效方式,但它们也强制所有属性都匹配其返回类型。这是因为字符串索引声明 obj.property 也可用作 obj[“property”]。在以下示例中,名称的类型与字符串索引的类型不匹配,类型检查器会出错:

这似乎是说这是不可能用打字稿表达的,这似乎是一个严重的限制。 Flow 做了我认为正确的事情,并假设 marginLeft 不属于索引规范。

这在 TypeScript 中是否可行?或者,有没有办法指定一个字符串是任何字符串是一组字符串?这样,我可以做一些大致如下的事情:

interface NestedCSSProperties: CSSProperties {
  [P not in keyof CSSProperties]?: CSSProperties
}

【问题讨论】:

  • 感谢大家的建议。不幸的是,我对建议的解决方法不感兴趣,因为它会使类型的声音明显降低。我已经打开了issue,并建议以某种方式实现这一点。

标签: typescript recursion


【解决方案1】:

这里的问题实际上不是递归(这是允许的),而是您指出的冲突签名。

我不确定当前行为的反面是否正确。对我来说,这似乎是一个主观决定,可以通过两种方式进行。按原样接受示例意味着您正在隐式合并定义;您可以查看其中一条线并假设界面的效果,而另一条线会改变结果。写起来确实感觉更自然,但我不确定它是否像您通常期望的那样安全,类型定义会失败。

无论如何。 TypeScript 确实允许与您期望的行为类似的行为,但您必须明确,因为字符串键也可以是stringnumber 类型。这将起作用:

interface CSSProperties {
    marginLeft?: string | number,
    [key: string]: CSSProperties|string|number,
}

比如上面的接口,这是有效的:

let a: CSSProperties = {
    marginLeft: 10,
    name: {
        marginLeft: 20,
    }
};

这不是:

let a: CSSProperties = {
    marginLeft: 10,
    something: false, // Type 'boolean' is not assignable to type 'string | number | CSSProperties'.
    something: new RegExp(/a/g), // Type 'RegExp' is not assignable to type 'CSSProperties'.
    name: {
        marginLeft: 20,
    },
    car: ["blue"], // Type 'string[]' is not assignable to type 'CSSProperties'.
};

它会正确认识命名的成员:

let name1: string | number = a.marginLeft; // OK, return type is string | number
a.marginLeft = false; // Blocked, Type 'false' is not assignable to type 'string | number'.
a["whatever"] = false; // Blocked, Type 'false' is not assignable to type 'string | number | CSSProperties'.
a["marginLeft"] = false; // Blocked, Type 'false' is not assignable to type 'string | number'.

然而,这里的问题是您需要在阅读时强制转换其他动态成员 - 它不会知道它是 CSSProperties

这不会被阻止:

a["whatever"] = 100;

它会抱怨这个:

let name3: CSSProperties = a["name"]; // Type is CSSProperties | string | number

但是,如果您明确地进行类型转换,这将起作用:

let name3: CSSProperties = a["name"] as CSSProperties;

【讨论】:

  • 我可以看到它是如何被认为是主观的,但这使得这种类型无法以它的方式表达。您的建议是我现在用作解决方法的方法,但是,正如您所指出的,它远不及声音。事实上,我可以拼错 CSS 属性而不会收到投诉。
【解决方案2】:

在这种情况下,您必须包含字符串和数字。然后你可以定义你的覆盖。使用此示例,如果您尝试将地图分配给“marginLeft”,TS 会抱怨,但会允许其他所有内容。

如果有更多限制,您可能需要编写更深入的 .d.ts 文件,该文件可能会使用映射类型。

您可以使用 Pick 等预设映射类型来简化您的专属属性,但我承认,有时我很难将自己的大脑围绕在它们周围。

interface IMap<T> {
    [key: string]: T
}

type CSSPropertyValue = string | number | CSSPropertiesBase;

interface CSSPropertiesBase extends IMap<CSSPropertyValue>
{

}

interface CSSProperties extends CSSPropertiesBase
{
    marginLeft?: string|number;
}

【讨论】:

  • 我很确定您会遇到 OP 遇到的与 string|number|undefined not assignable to CssPropertiesBase 相同的问题
【解决方案3】:

你试过像这样使用交叉类型吗?

interface CssPropertyValues {
    marginLeft? :string |number;    
}

interface RecursiveCssProperties { 
    [key: string]:  CssProperties;
}

type CssProperties = CssPropertyValues & RecursiveCssProperties;

let foo: CssProperties = {};

let myMargin = foo.bar.really.foobar.marginLeft; //should work... myMargin is typed as string|number|undefined

交集类型是接口继承的一个经常被忽视的替代方案,但功能稍强

【讨论】:

  • 嘿,嘿,丹。不幸的是,这不起作用,它与我的示例具有完全相同的效果,请尝试使用:let foo: CssProperties = { marginLeft: 3 }
  • 啊...奇怪的是,如果您访问属性,它会起作用,(例如var foo: CssProperties = {}; foo.marginLeft = 3;
  • 在您提交的问题得到解决之前,我认为这是解决问题的最不差的方法。您可以使用任播敲定分配,然后所有以后的属性访问都按预期工作。 (如果你想描述一些新的东西,情况会更糟,但这不是你的问题。)
猜你喜欢
  • 2021-07-10
  • 1970-01-01
  • 1970-01-01
  • 2017-12-12
  • 2019-07-27
  • 2018-12-15
  • 2022-01-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多