简体   繁体   English

带索引器的Typescript递归类型

[英]Typescript recursive type with indexer

I'd like to define a type that is recursive on itself like this, essentially: 我想定义一个这样的递归类型,基本上是这样的:

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

Unfortunately, the typescript docs say: 不幸的是, 打字稿文档说:

While string index signatures are a powerful way to describe the “dictionary” pattern, they also enforce that all properties match their return type. 尽管字符串索引签名是描述“字典”模式的强大方法,但它们也强制所有属性与其返回类型匹配。 This is because a string index declares that obj.property is also available as obj[“property”]. 这是因为字符串索引声明obj.property也可以作为obj [“ property”]使用。 In the following example, name's type does not match the string index's type, and the type-checker gives an error: 在以下示例中,名称的类型与字符串索引的类型不匹配,并且类型检查器给出错误:

Which seems to say this is impossible to express in typescript, which seems a severe limitation. 这似乎无法用打字稿来表达,这似乎是一个严重的局限。 Flow does what I consider the right thing here and assumes that marginLeft does not fall into the index specification. Flow在这里做了我认为正确的事情,并假设marginLeft不属于索引规范。

Is this possible at all in TypeScript? 在TypeScript中根本可以做到吗? Alternatively, is there a way to specify that a string is any string but a set of strings? 或者,是否可以指定一个字符串是一组字符串之外的任何字符串? That way, I could do something roughly like: 这样,我可以做大概的事情:

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

The problem here is not really with recursion (which is allowed) but with the conflicting signatures as you pointed out. 这里的问题不是真正的递归(允许),而是您指出的签名冲突。

I'm not sure the opposite of the current behavior would be correct. 我不确定当前行为的反面是否正确。 It seems like a subjective decision to me with cases that can be made for both ways. 对于我来说,这似乎是一种主观决定,可以同时为两种方式做出案例。 Accepting the example as-is means you're merging the definitions implicitly; 照原样接受示例意味着您将隐式合并定义。 you may look at one of the lines and assume an effect of the interface while another line changes the outcome. 您可以查看其中的一行,并假定界面的效果,而另一行更改结果。 It does feel more natural to write but I'm not sure it's as safe as you don't normally expect fall throughs on type definitions. 编写起来确实感觉更自然,但是我不确定它是否像您通常不会期望的那样安全。

Regardless. 而不管。 TypeScript does allow a similar behavior as to what you're expecting, but you have to be explicit in that string keys can also be of type string or number . TypeScript确实具有与您期望的行为类似的行为,但是必须明确指出,字符串键也可以是stringnumber类型。 This will work: 这将起作用:

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

For example, with the above interface, this is valid: 例如,对于上面的接口,这是有效的:

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

This is not: 这不是:

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'.
};

It'll know the named members correctly: 它会正确地知道命名成员:

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'.

The problem here, however, is that you need to cast other dynamic members when reading - it won't know it's a CSSProperties . 但是,这里的问题是,您需要在读取时CSSProperties其他动态成员-它不知道这是CSSProperties

This won't get blocked: 这不会被阻止:

a["whatever"] = 100;

And it will complain about this: 它会抱怨:

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

But this will work if you typecast explicitly: 但是,如果您明确键入,这将起作用:

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

In this case you have to be inclusive of string and number. 在这种情况下,您必须包含字符串和数字。 Then you can define your overrides. 然后,您可以定义替代。 Using this example, if you try to assign a map to 'marginLeft', TS will complain, but will be permissive of everything else. 使用此示例,如果您尝试将地图分配给'marginLeft',则TS会投诉,但会允许其他所有事情。

Any more constraining and you'll probably need to write a more in-depth .d.ts file which may use mapped types. 任何其他限制,您可能需要编写更深入的.d.ts文件,该文件可能使用映射的类型。

You could simplify your exclusive properties using the preset mapped types like Pick but I admit, I have a hard time wrapping my brain around them sometimes. 您可以使用诸如Pick的预设映射类型来简化专有属性,但我承认,有时候我很难将它们包裹在它们周围。

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

type CSSPropertyValue = string | number | CSSPropertiesBase;

interface CSSPropertiesBase extends IMap<CSSPropertyValue>
{

}

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

Have you tried using an intersection type Like this? 您是否尝试过使用像这样的路口类型?

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

Intersection types are an often over-looked and slightly more powerful alternative to interface inheritance 交集类型是接口继承的一种经常被忽略且功能更强大的替代方法

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM