如何在打字稿中使用已知类型对1个已知属性和未知属性名称进行建模

时间:2019-05-22 15:41:49

标签: typescript

如何对具有1个已知属性和1个属性的模型进行建模,而我知道类型,但是不知道属性名称。

我认为它应该是递归类型,但是我不确定如何在打字稿中做到这一点。

所以我有这种安排:

export const hierarchy: Hierarchy = {
  location: {
    name: "location",
    regions: [
      {
        name: "West Coast",
        states: [
          {
            name: "California",
            cities: [
              { name: "San Diego", sites: [{name: "site 1"},
              { name: "Los Angeles" },
              { name: "San Jose" },
            ],
          },
        ],
      }
    ],
  },
};

因此,每个对象都有一个名称字段和一个相同类型且深层次的数组。

这是我幼稚的尝试,显然不起作用:

export interface Graph<T> {
  name: string;
  [key: string]: Graph[]
}

1 个答案:

答案 0 :(得分:0)

TypeScript目前尚无法将directly express或除string以外的所有'name'作为一种类型,更不用说key type of an index signature或映射类型。因此,您想要说类似:

// this is not valid TypeScript (yet?)
interface Graph { 
  name: string, 
  [k: string & not "name"]: Array<Graph> 
}

但是你不能。也许这最终会发生,但是现在这只是一个梦想。


您可以 使用mappedconditional类型将Graph表示为对对象类型的约束。以及generics。这个想法是,您拥有通用类型别名Graph而不是VerifyGraph<T>,该别名将您希望有效的T的候选类型作为参数Graph。如果T 是有效的Graph,则VerifyGraph<T>将与T相同。否则,将通过修复任何不正确的属性将VerifyGraph<T>更改为有效的Graph。最后,创建一个通用辅助函数asGraph<T>(),该函数采用类型T & VerifyGraph<T>的参数。编译器将推断类型T作为参数的类型,然后它将检查VerifyGraph<T>以确保其也符合。如果可以,那么很好,因为T & VerifyGraph<T>只会是T & TT。否则,您会收到一条错误消息,因为T & VerifyGraph<T>不是T

此外,由于您似乎正在创建一个包含Hierarchy的{​​{1}},因此我们可以将其表示为具有自己的辅助函数的泛型类型:

Graph

它在这里工作:

type VerifyGraph<T> = T extends { name: string }
    ? { [K in keyof T]:
        K extends "name" ? string :
        T[K] extends Array<infer E> ? Array<VerifyGraph<E>> :
        Array<{ name: string }>
    } : { name: string }

const asGraph = <T>(x: T & VerifyGraph<T>): T => x;

interface Hierarchy<T> {
    location: T & VerifyGraph<T>
}

const asHierarchy = <T>(x: Hierarchy<T>): Hierarchy<T> => x;

这是一些错误:

// okay
const hierarchy = asHierarchy({
    location: {
        name: "location",
        regions: [
            {
                name: "West Coast",
                states: [
                    {
                        name: "California",
                        cities: [
                            {
                                name: "San Diego",
                                sites: [
                                    { name: "site 1" },
                                    { name: "Los Angeles" },
                                    { name: "San Jose" },
                                ],
                            },
                        ],
                    }
                ],
            },
        ]
    }
});

我想所有人都应有表现。

Link to code


这可能比您想在代码中使用的更为复杂。这里最大的缺点是const badHierarchy1 = asHierarchy({ location: { name: "Okay", notAnArray: 123 // error! // ~~~~~~~~~~ <-- Type 'number' is not assignable to type '{ name: string; }[]'. } }) const badHierarchy2 = asHierarchy({ location: { // error! // ~~~~~~~~ <-- Property 'name' is missing in type '{ noName: never[]; }' noName: [] } }) const badHierarchy3 = asHierarchy({ location: { name: "level1", stuff: [ { name: "level2", deeperNotAnArray: "oopsie"; // error! // ~~~~~~~~~~~~~~~~ <-- // Type 'string' is not assignable to type '{ name: string; }[]'. } ] } }) 不再是具体类型,而是通用类型,因此处理它的每个函数和类型都需要添加通用参数。因此,您可能只想将此限制放在用户调用的函数中以传递某些内容,然后在处理该函数的库中使用诸如

这样的松散类型
Graph

相反。每个有效的interface LooseGraph { name: string, [k: string]: string | Array<LooseGraph> } 都可以分配给T & Graph<T>,反之亦然,因此您可能必须在库中做一些断言。但这是我认为您目前可以使用TypeScript做到的最好的选择。

好的,希望能有所帮助;祝你好运!