打字稿:嵌套对象的深层关键字

时间:2019-10-17 13:56:02

标签: typescript

所以我想找到一种方法来拥有嵌套对象的所有键。

我有一个接受参数类型的泛型类型。我的目标是获取给定类型的所有键。

在这种情况下,以下代码运行良好。但是当我开始使用嵌套对象时,情况就不同了。

type SimpleObjectType = {
  a: string;
  b: string;
};

// works well for a simple object
type MyGenericType<T extends object> = {
  keys: Array<keyof T>;
};

const test: MyGenericType<SimpleObjectType> = {
  keys: ['a'];
}

这是我想要实现的目标,但是没有用。

type NestedObjectType = {
  a: string;
  b: string;
  nest: {
    c: string;
  };
  otherNest: {
    c: string;
  };
};

type MyGenericType<T extends object> = {
  keys: Array<keyof T>;
};

// won't works => Type 'string' is not assignable to type 'a' | 'b' | 'nest' | 'otherNest'
const test: MyGenericType<NestedObjectType> = {
  keys: ['a', 'nest.c'];
}

那么,在不使用功能的情况下,我该怎么办才能将此类键提供给test

4 个答案:

答案 0 :(得分:5)

如前所述,当前无法在类型级别连接字符串文字。有一些建议可能允许这样做,例如a suggestion to allow augmenting keys during mapped typesa suggestion to validate string literals via regular expression,但是目前这是不可能的。

您可以将路径表示为字符串文字的tuples,而不是将路径表示为点缀字符串。因此"a"变成["a"],而"nest.c"变成["nest", "c"]。在运行时,通过split()join()方法在这些类型之间进行转换很容易。


因此,您可能希望使用类似Paths<T>的方法,该方法返回给定类型T的所有路径的并集,或者可能返回Leaves<T>,而这只是Paths<T>的那些元素指向非对象类型本身。这种类型没有内置的支持。 ts-toolbelthas this,但是由于我无法在Playground中使用该库,因此我将在此处滚动显示自己的库。

请注意:PathsLeaves本质上是递归的,因此可能会对编译器造成很大的负担。而且TypeScript也未正式支持为此所需的递归类型。我将在下面介绍的是以这种不确定的/不是真正支持的方式进行递归的,但是我尝试为您提供一种指定最大递归深度的方法。

我们在这里:

type Cons<H, T> = T extends readonly any[] ?
    ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never
    : never;

type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
    11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]

type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    { [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
        P extends [] ? never : Cons<K, P> : never
    ) }[keyof T]
    : [];


type Leaves<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
    { [K in keyof T]-?: Cons<K, Leaves<T[K], Prev[D]>> }[keyof T]
    : [];

Cons<H, T>的目的是采用任何类型的H和一个元组类型的T,并产生一个新的元组,其中将H放在T之前。因此Cons<1, [2,3,4]>应该是[1,2,3,4]。该实现使用rest/spread tuples。我们将需要它来建立路径。

类型Prev是一个长元组,可用于获取前一个数字(最大)。因此Prev[10]9,而Prev[1]0。在深入对象树时,我们将需要使用它来限制递归。

最后,通过进入每种对象类型Paths<T, D>并收集键,然后Leaves<T, D>将它们移到TCons上来实现PathsLeaves这些键的Paths个属性。它们之间的区别在于D也直接在联合中包括子路径。默认情况下,深度参数10D,并且在每次降级时,我们将0减小1,直到尝试超过type NestedObjectPaths = Paths<NestedObjectType>; // type NestedObjectPaths = [] | ["a"] | ["b"] | ["c"] | // ["nest"] | ["nest", "c"] | ["otherNest"] | ["otherNest", "c"] type NestedObjectLeaves = Leaves<NestedObjectType> // type NestedObjectLeaves = ["a"] | ["b"] | ["nest", "c"] | ["otherNest", "c"] ,然后停止递归。


好的,让我们对其进行测试:

interface Tree {
    left: Tree,
    right: Tree,
    data: string
}

要了解深度限制的用途,请想象我们有一个像这样的树类型:

Leaves<Tree>

嗯,type TreeLeaves = Leaves<Tree>; // sorry, compiler ?⌛? // type TreeLeaves = ["data"] | ["left", "data"] | ["right", "data"] | // ["left", "left", "data"] | ["left", "right", "data"] | // ["right", "left", "data"] | ["right", "right", "data"] | // ["left", "left", "left", "data"] | ... 2038 more ... | [...] 很大:

type TreeLeaves = Leaves<Tree, 3>;
// type TreeLeaves2 = ["data"] | ["left", "data"] | ["right", "data"] |
// ["left", "left", "data"] | ["left", "right", "data"] | 
// ["right", "left", "data"] | ["right", "right", "data"]

,编译器生成它会花费很长时间,并且编辑器的性能突然变得非常非常差。让我们将其限制在更易于管理的位置:

Paths

这迫使编译器停止查看深度3,因此所有路径的长度最多为3。


所以,这可行。 ts-toolbelt或其他实现很可能会更小心,不要使编译器心脏病发作。因此,我不必说您应该在未经重大测试的情况下在生产代码中使用它。

但是无论如何,假设您拥有并想要type MyGenericType<T extends object> = { keys: Array<Paths<T>>; }; const test: MyGenericType<NestedObjectType> = { keys: [['a'], ['nest', 'c']] } ,这就是您想要的类型:

  File yourFile =  File yourFile = new File("C:\\Users\\" + user + "\\" + filename + ".csv");
    yourFile.getParentFile().mkdirs();
    try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(yourFile))) {

        for (int i = 0; i < saveData.size(); i++) {
            dos.writeInt(saveData.get(i));
        }

    } catch (IOException e) {
        e.printStackTrace();
    }

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

Link to code

答案 1 :(得分:4)

我遇到了类似的问题,当然,上面的答案非常惊人。但对我来说,它有点过头了,并且如前所述对编译器来说是相当繁重的。

虽然不那么优雅,但更易于阅读,但我建议使用以下类型来生成类似路径的元组:

type PathTree<T> = {
  [P in keyof T]: T[P] extends object ? [P?, ...Path<T[P]>] : [P?];
};

type Path<T> = PathTree<T>[keyof PathTree<T>];

一个主要的缺点是,这种类型不能处理自引用类型,比如来自@jcalz 的 Tree 回答:

interface Tree {
  left: Tree,
  right: Tree,
  data: string
};

type TreePath = Path<Tree>;
// Type of property 'left' circularly references itself in mapped type 'PathTree<Tree>'.ts(2615)
// Type of property 'right' circularly references itself in mapped type 'PathTree<Tree>'.ts(2615)

但对于其他类型,它似乎做得很好:

interface OtherTree {
  nested: {
    props: {
      a: string,
      b: string,
    }
    d: number,
  }
  e: string
};

type OtherTreePath = Path<OtherTree>;
// [("nested" | undefined)?, ("props" | undefined)?, ("a" | undefined)?] |
// [("nested" | undefined)?, ("props" | undefined)?, ("b" | undefined)?] |
// [("nested" | undefined)?, ("d" | undefined)?] |
// [("e" | undefined)?]

如果您只想强制引用叶节点,您可以删除 PathTree 类型中的可选修饰符:

type PathTree<T> = {
  [P in keyof T]: T[P] extends object ? [P, ...Path<T[P]>] : [P];
};

type OtherPath = Path<OtherTree>;
// ["nested", "props", "a"] | ["nested", "props", "b"] | ["nested", "d"] | ["e"]

对于一些更复杂的对象,不幸的是,类型似乎默认为 [...any[]],另外可能需要排除未定义的类型:

type Path<T> = Exclude<PathTree<T>[keyof PathTree<T>], undefined>;

答案 2 :(得分:0)

因此上述解决方案确实有效,但是,它们要么语法有些混乱,要么给编译器带来了很大压力。以下是针对您只需要一个字符串的用例的编程建议:

type PathSelector<T, C = T> = (C extends {} ? {
    [P in keyof C]: PathSelector<T, C[P]>
} : C) & {
    getPath(): string
}

function pathSelector<T, C = T>(path?: string): PathSelector<T, C> {
    return new Proxy({
        getPath() {
            return path
        },
    } as any, {
        get(target, name: string) {
            if (name === 'getPath') {
                return target[name]
            }
            return pathSelector(path === undefined ? name : `${path}.${name}` as any)
        }
    })
}

type SomeObject = {
    value: string
    otherValue: string
    child: SomeObject
    otherChild: SomeObject
}
const path = pathSelector<SomeObject>().child.child.otherChild.child.child.otherValue
console.log(path.getPath())// will print: "child.child.otherChild.child.child.otherValue"
function doSomething<T, K>(path: PathSelector<T, K>, value: K){
}
// since otherValue is a number:
doSomething(path, 1) // works
doSomething(path, '1') // Error: Argument of type 'string' is not assignable to parameter of type 'number'

类型参数 T 将始终保持与原始请求对象的类型相同,以便可以使用它来验证路径是否确实来自指定的对象。

C 表示路径当前指向的字段类型

答案 3 :(得分:0)

基于conditional types使用template literalmapped types字符串、index access types@jcalz's answer的递归类型函数,可以用这个ts playground example进行验证

生成联合类型的属性,包括用点表示法嵌套

type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`

type DotNestedKeys<T> = (T extends object ?
    { [K in Exclude<keyof T, symbol>]: `${K}${DotPrefix<DotNestedKeys<T[K]>>}` }[Exclude<keyof T, symbol>]
    : "") extends infer D ? Extract<D, string> : never;

/* testing */

type NestedObjectType = {
    a: string
    b: string
    nest: {
        c: string;
    }
    otherNest: {
        c: string;
    }
}

type NestedObjectKeys = DotNestedKeys<NestedObjectType>
// type NestedObjectKeys = "a" | "b" | "nest.c" | "otherNest.c"

const test2: Array<NestedObjectKeys> = ["a", "b", "nest.c", "otherNest.c"]

这在使用 mongodbfirebase firestore 等文档数据库时也很有用,这些数据库允许使用点表示法设置单个嵌套属性

使用 mongodb

db.collection("products").update(
   { _id: 100 },
   { $set: { "details.make": "zzz" } }
)

带火力

db.collection("users").doc("frank").update({
   "age": 13,
   "favorites.color": "Red"
})

可以使用此类型创建此更新对象

然后打字稿会引导你,只需添加你需要的属性

export type DocumentUpdate<T> = Partial<{ [key in DotNestedKeys<T>]: any & T}> & Partial<T>

enter image description here

您还可以更新 do 嵌套属性生成器以避免显示嵌套属性数组、日期...

type DotNestedKeys<T> =
T extends (ObjectId | Date | Function | Array<any>) ? "" :
(T extends object ?
    { [K in Exclude<keyof T, symbol>]: `${K}${DotPrefix<DotNestedKeys<T[K]>>}` }[Exclude<keyof T, symbol>]
    : "") extends infer D ? Extract<D, string> : never;