递归转换typescript中对象树的所有叶子

递归转换typescript中对象树的所有叶子,typescript,tree,Typescript,Tree,给定一个简单的对象树,该对象树包含其自身类型的值或需要转换的类型的值: 接口树{ [键:字符串]:树|叶; } 我想定义一个函数,递归地将所有叶子转换为另一种类型,如下所示: const t1 = { a: "A", b: { c: "CC", d: { e: "EEE" } } }; const t2 = transformTree(t1, (x: string) => x.length, (v): v is string => typeof v === "str

给定一个简单的对象树,该对象树包含其自身类型的值或需要转换的类型的值:

接口树{
[键:字符串]:树|叶;
}
我想定义一个函数,递归地将所有叶子转换为另一种类型,如下所示:

const t1 = { a: "A", b: { c: "CC", d: { e: "EEE" } } };
const t2 = transformTree(t1,
    (x: string) => x.length,
    (v): v is string => typeof v === "string"
); 
t2.a; // number
t2.b.c; // number
t2.b.d.e; // number
type TransformTreeIdx<T, X, K extends keyof X> = { [P in keyof T]:
    T[P] extends X ? X[K] :
    TransformTreeIdx<T[P], X, K>;
};
declare function transformTreeIdx<TX, X, K extends keyof X>(
    obj: TX,
    key: K,
    isLeaf: (value: any) => value is X
): TransformTreeIdx<TX, X, K>;
函数转换树(
obj:树,
变换:(值:S)=>T,
isLeaf:(值:S |树)=>布尔值
):树{
返回Object.assign(
{},
…Object.entries(obj.map)([key,value])=>({
[键]:isLeaf(值)
?转换(值为S)
:transformTree(值为Tree、transform、isLeaf),
}))
);
}
如何维护源树和转换树之间的叶子类型

测试上述内容不起作用:

类包装器{
构造函数(公共值:T){}
}
函数转换(包装:包装):T{
返回值;
}
函数展开(包装:树):树{
返回转换树(
包裹,
使改变
(值:Wrapper | Tree)=>Wrapper的值实例
);
}
const obj=展开({
傅:{
酒吧:新包装(“baz”),
},
奶牛:新包装(“moo”),
});
功能把手(值:“baz”){
返回true;
}
函数handleMoo(值:“moo”){
返回true;
}
把手(obj.foo.bar);//错误:(162,19)TS2339:类型“string | Tree”上不存在属性“bar”。类型“string”上不存在属性“bar”。
handleMoo(对象为奶牛)//错误:(163,11)TS2345:类型为'string | Tree'的参数不能分配给类型为'moo'的参数。类型“string”不可分配给类型“moo”。
我可以看到出现了错误,因为树结构不是通过转换来维护的(转换只在运行时起作用)。但是考虑到输入树的已知结构,转换是可预测的,我觉得应该有一种方法在typescript中实现这一点

以下是我试图解决的问题:

类型转换树={
[K in keyof InputTree]:InputTree[K]扩展叶
T
:InputTree[K]扩展树
?转换树
:从不;
};
类型IsLeaf=(值:树|叶)=>布尔值;
函数转换树(
树:T,
转换:(值:从)=>到,
isLeaf:isLeaf
):转换树{
返回Object.assign(
{},
…Object.entries(tree.map)([key,value])=>({
//XXX必须在每种情况下转换值,因为typescript无法预测
//isLeaf()的结果。
[键]:isLeaf(值)
?转换(提取值)
:转换树(
作为提取物的价值,
使改变
岛屿
),
}))
);
}
它似乎仍然不理解嵌套类型:


函数展开(
包装:树
):转换树{
返回转换树(
包裹,
使改变
(值:Wrapper | Tree)=>Wrapper的值实例
);
}
功能把手(值:“baz”){
返回true;
}
函数handleMoo(值:“moo”){
返回true;
}
handleMoo(对象为奶牛);//好啊
把手(obj.foo.bar);//TS2339:类型“从不”上不存在属性“bar”。


似乎typescript仍然认为,如果字段值不是叶子,那么它可能不是子树。

在下面的内容中,我只关心键入而不是实现。所有函数都将是
declare
d,就好像实际的实现在某个JS库中,而这些就是它们的实现一样

另外,
isLeaf
函数的类型可能应该是返回类型为类型谓词的函数,而不仅仅是
boolean
。函数签名
isLeaf:(value:S | Tree)=>value is S
与返回
boolean
的函数签名类似,只是编译器实际上会理解在
if(isLeaf(x)){x}else{x}
中,真块中的
x
将是
S
,假块中的
x
将是

好的,下面是:


类型
过于笼统,无法跟踪特定的键和值类型。编译器只知道该类型的值,比如说,
Tree
,它是一个对象类型,其属性是类型
string
Tree
。一旦你这样做了,比如说:

const x: Tree<string> = { a: "", b: { c: "", d: { e: "" } } };
如果您所关心的只是提出一种类型转换,这种转换可以维护嵌套的键结构,并将
树的某个子类型转换为具有相同形状的
树的子类型,那么您可以这样做。但在最直接的实现中,生成的树的所有叶子都将是
Y
类型,而不是更窄的类型。我是这样写的:

type TransformTree<T extends Tree<X>, X, Y> = { [K in keyof T]:
    T[K] extends X ? Y :
    T[K] extends Tree<X> ? TransformTree<T[K], X, Y> :
    T[K];
};
declare function transformTree<X, Y, TX extends Tree<X>>(
    obj: TX,
    transform: (value: X) => Y,
    isLeaf: (value: X | Tree<X>) => value is X
): TransformTree<TX, X, Y>;

但你希望这里有更雄心勃勃的东西;似乎您不仅希望叶变换映射从特定类型
X
到特定类型
Y
,还希望指定一些常规类型函数,如
type F=…
,并将叶从类型
Z扩展X
映射到
F

在您的示例中,您的输入类型类似于
Wrapped,虽然拥有它们会让人惊讶,但在可预见的未来似乎不会发生


您可以做的是设想特定类型的叶类型转换,并为它们实现特定版本的
TransformTree
。例如,如果叶类型映射只是索引到单个属性中,如
type F=T[K]
,如
Unwrap
中所示,则可以这样编写:

const t1 = { a: "A", b: { c: "CC", d: { e: "EEE" } } };
const t2 = transformTree(t1,
    (x: string) => x.length,
    (v): v is string => typeof v === "string"
); 
t2.a; // number
t2.b.c; // number
t2.b.d.e; // number
type TransformTreeIdx<T, X, K extends keyof X> = { [P in keyof T]:
    T[P] extends X ? X[K] :
    TransformTreeIdx<T[P], X, K>;
};
declare function transformTreeIdx<TX, X, K extends keyof X>(
    obj: TX,
    key: K,
    isLeaf: (value: any) => value is X
): TransformTreeIdx<TX, X, K>;
并使用它:

const u = {
    foo: {
        bar: "baz" as const,
    },
    cow: "moo" as const,
};

const u2 = transformTreeWrap(
    u, (x: any): x is string => typeof x === "string"
);
handleBaz(u2.foo.bar.value);
handleMoo(u2.cow.value);

或者,您可能有一个
isLeaf
/
transform
对数组,允许针对不同的更具体的转换测试每个节点。因此,例如,每当您在树中找到一个
“moo”
值时,您就会输出一个
数字
,每当您找到一个
“baz”
值时
const u = {
    foo: {
        bar: "baz" as const,
    },
    cow: "moo" as const,
};

const u2 = transformTreeWrap(
    u, (x: any): x is string => typeof x === "string"
);
handleBaz(u2.foo.bar.value);
handleMoo(u2.cow.value);
type TransformTreeMap<T, M extends [any, any]> = { [K in keyof T]:
    T[K] extends M[0] ? Extract<M, [T[K], any]>[1] :
    TransformTreeMap<T[K], M> };

type IsLeafAndTransformer<I, O> = {
    isLeaf: (x: any) => x is I,
    transform: (x: I) => O
}
type TransformArrayToMap<M extends Array<IsLeafAndTransformer<any, any>>> = {
    [K in keyof M]: M[K] extends IsLeafAndTransformer<infer I, infer O> ?
    [I, O] : never }[number]

declare function transformTreeMap<T, M extends Array<IsLeafAndTransformer<any, any>>>(
    obj: T,
    ...transformers: M
): TransformTreeMap<T, TransformArrayToMap<M>>;
const mm = transformTreeMap(u,
    { isLeaf: (x: any): x is "moo" => x === "moo", transform: (x: "moo") => 123 },
    { isLeaf: (x: any): x is "baz" => x === "baz", transform: (x: "baz") => true }
);
mm.cow // number
mm.foo.bar // boolean
function leafsToString<O extends Record<any, any>>(obj: O): unknown {
    const newObj = obj instanceof Array ? [] : Object.create(null);
    for (const key of Object.keys(obj)) {
        if (typeof obj[key] === 'object') {
            newObj[key] = leafsToString(obj[key]);
        } else {
            newObj[key] = obj[key] + '';
        }
    }
    return newObj;
}
type LeafsToString<O> = {
    [K in keyof O]:
        O[K] extends Record<any, any>
        ? LeafsToString<O[K]>
        : string
}