所以我想找到一种方法来拥有嵌套对象的所有键。
我有一个接受参数类型的泛型类型。我的目标是获取给定类型的所有键。
在这种情况下,以下代码运行良好。但是当我开始使用嵌套对象时,情况就不同了。
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
?
答案 0 :(得分:5)
如前所述,当前无法在类型级别连接字符串文字。有一些建议可能允许这样做,例如a suggestion to allow augmenting keys during mapped types和a 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-toolbelt库has this,但是由于我无法在Playground中使用该库,因此我将在此处滚动显示自己的库。
请注意:Paths
和Leaves
本质上是递归的,因此可能会对编译器造成很大的负担。而且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>
将它们移到T
和Cons
上来实现Paths
和Leaves
这些键的Paths
个属性。它们之间的区别在于D
也直接在联合中包括子路径。默认情况下,深度参数10
为D
,并且在每次降级时,我们将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();
}
希望有所帮助;祝你好运!
答案 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 literal、mapped 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"]
这在使用 mongodb 或 firebase 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>
您还可以更新 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;