在打字稿中递归地转换对象树的所有叶子

d_i*_*ble 4 tree typescript

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

interface Tree<Leaf> {
  [key: string]: Tree<Leaf> | Leaf;
}
Run Code Online (Sandbox Code Playgroud)

我想定义一个函数,将所有叶子递归地转换为另一种类型,如下所示:

function transformTree<S, T>(
  obj: Tree<S>,
  transform: (value: S) => T,
  isLeaf: (value: S | Tree<S>) => boolean
): Tree<T> {
  return Object.assign(
    {},
    ...Object.entries(obj).map(([key, value]) => ({
      [key]: isLeaf(value)
        ? transform(value as S)
        : transformTree(value as Tree<S>, transform, isLeaf),
    }))
  );
}
Run Code Online (Sandbox Code Playgroud)

如何维护源树和转换树之间叶子的类型?

测试以上不起作用:

class Wrapper<T> {
  constructor(public value: T) {}
}

function transform<T>(wrapped: Wrapper<T>): T {
  return wrapped.value;
}

function unwrap<T>(wrapped: Tree<Wrapper<T>>): Tree<T> {
  return transformTree<Wrapper<T>, T>(
    wrapped,
    transform,
    (value: Wrapper<T> | Tree<Wrapper<T>>) => value instanceof Wrapper
  );
}

const obj = unwrap<string>({
  foo: {
    bar: new Wrapper("baz"),
  },
  cow: new Wrapper("moo"),
});

function handleBaz(value: "baz") {
  return true;
}

function handleMoo(value: "moo") {
  return true;
}

handleBaz(obj.foo.bar); // Error:(162, 19) TS2339: Property 'bar' does not exist on type 'string | Tree<string>'. Property 'bar' does not exist on type 'string'.
handleMoo(obj.cow); //Error:(163, 11) TS2345: Argument of type 'string | Tree<string>' is not assignable to parameter of type '"moo"'. Type 'string' is not assignable to type '"moo"'.
Run Code Online (Sandbox Code Playgroud)

我可以看到发生了错误,因为树结构不是通过转换维护的(转换仅在运行时起作用)。但鉴于输入树的已知结构,转换是可预测的,我认为应该有一种方法可以在打字稿中做到这一点。

这是我解决这个问题的尝试:

type TransformTree<Leaf, InputTree extends Tree<Leaf>, T> = {
  [K in keyof InputTree]: InputTree[K] extends Leaf
    ? T
    : InputTree[K] extends Tree<Leaf>
    ? TransformTree<Leaf, InputTree[K], T>
    : never;
};

type IsLeaf<Leaf> = (value: Tree<Leaf> | Leaf) => boolean;

function transformTree<T extends Tree<From>, From, To>(
  tree: T,
  transform: (value: From) => To,
  isLeaf: IsLeaf<From>
): TransformTree<From, typeof tree, To> {
  return Object.assign(
    {},
    ...Object.entries(tree).map(([key, value]) => ({
      // XXX have to cast the value in each case, because typescript cannot predict
      // the outcome of isLeaf().
      [key]: isLeaf(value)
        ? transform(value as Extract<typeof tree[typeof key], From>)
        : transformTree(
            value as Extract<typeof tree[typeof key], Tree<From>>,
            transform,
            isLeaf
          ),
    }))
  );
}

Run Code Online (Sandbox Code Playgroud)

它似乎仍然不理解嵌套类型:


function unwrap<T>(
  wrapped: Tree<Wrapper<T>>
): TransformTree<Wrapper<T>, typeof wrapped, T> {
  return transformTree(
    wrapped,
    transform,
    (value: Wrapper<T> | Tree<Wrapper<T>>) => value instanceof Wrapper
  );
}


function handleBaz(value: "baz") {
  return true;
}

function handleMoo(value: "moo") {
  return true;
}

handleMoo(obj.cow); // OK

handleBaz(obj.foo.bar); // TS2339: Property 'bar' does not exist on type 'never'.

Run Code Online (Sandbox Code Playgroud)

游乐场链接

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

jca*_*alz 5

接下来,我将只关心打字而不是实现。所有函数都将被declare编译,就好像实际的实现在某个 JS 库中,并且这些是它们的声明文件

此外,您的isLeaf函数可能应该被键入为用户定义的类型保护,其返回类型是类型谓词而不仅仅是boolean。函数签名isLeaf: (value: S | Tree<S>) => value is S与返回的函数签名类似boolean,只是编译器实际上会理解 in if (isLeaf(x)) { x } else { x }xtrue 块中的Sxfalse 块中的Tree<S>

好的,这里是:


该类型Tree<X>过于笼统,无法跟踪特定的键和值类型。编译器所知道的关于类型值(例如 )的全部信息Tree<string>是,它是一个对象类型,其属性为类型stringTree<string>。一旦你这样做了,就说:

const x: Tree<string> = { a: "", b: { c: "", d: { e: "" } } };
Run Code Online (Sandbox Code Playgroud)

您已经丢弃了有关特定结构的所有详细信息以及有关string叶子的任何特定子类型的所有详细信息:

x.a.toUpperCase(); // error
x.z; // no error 
Run Code Online (Sandbox Code Playgroud)

如果您所关心的只是提出一个类型转换来维护嵌套的键结构并将 的某些子类型转换Tree<X>为具有相同形状的 的子类型Tree<Y>,那么您可以做到。但在最直接的实现中,生成的树的所有叶子都将是类型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>;
Run Code Online (Sandbox Code Playgroud)

您可以在一个简单的示例中看到它的工作原理,如下所示:

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
Run Code Online (Sandbox Code Playgroud)

但你想要一些更雄心勃勃的东西;似乎您不仅想要从特定类型X到特定类型的叶转换映射Y,而且还想指定一些通用类型函数,例如 type F<T extends X> = ...将叶从类型映射Z extends XF<Z>

在您的示例中,您的输入类型类似于Wrapped<any>,您的输出类型函数将类似于type F<T extends Wrapped<any> = T["value"];

不幸的是,这种更通用的转换类型无法用 TypeScript 表达。您不能拥有像type TransformType<T extends Tree<X>, X, F> = ...whereF本身就是带有参数的类型函数这样的类型函数。这需要语言支持所谓的“更高种类的类型”。microsoft/TypeScript#1213对此有一个长期的开放功能请求,虽然拥有它们会令人惊奇,但看起来在可预见的未来不会发生。


可以做的是设想特定类型的叶类型转换并为它们实现特定版本TransformTree。例如,如果您的叶类型映射仅索引到单个属性(type F<T extends Record<K, any>, K extends PropertyKey> = T[K]如您的Unwrap情况),那么您可以这样编写:

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>;
Run Code Online (Sandbox Code Playgroud)

然后使用它:

const w = {
    foo: {
        bar: new Wrapper("baz" as const),
    },
    cow: new Wrapper("moo" as const),
};

const w2 = transformTreeIdx(
    w, "value", (x: any): x is Wrapper<any> => x instanceof Wrapper
);

handleBaz(w2.foo.bar);
handleMoo(w2.cow);
Run Code Online (Sandbox Code Playgroud)

或者,正如您的评论中提到的,您可能想对特定的通用接口/类执行相反的操作,例如,Wrapper...映射将转换Z extends XWrapper<Z>

type TransformTreeWrap<T, X> = { [P in keyof T]:
    T[P] extends X ? Wrapper<T[P]> :
    TransformTreeWrap<T[P], X>;
};
declare function transformTreeWrap<TX, X, K extends keyof X>(
    obj: TX,
    isLeaf: (value: any) => value is X
): TransformTreeWrap<TX, X>;
Run Code Online (Sandbox Code Playgroud)

并使用它:

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);
Run Code Online (Sandbox Code Playgroud)

或者,您可能有一个isLeaf/对的transform数组,允许针对不同的更具体的转换来测试每个节点。因此,例如,每当您"moo"在树中找到一个值时,您都会输出 a number,而每当您"baz"在树中找到一个值时,您都会输出 a boolean

那么你的输入可能如下所示:

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>>;
Run Code Online (Sandbox Code Playgroud)

然后你使用它:

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
Run Code Online (Sandbox Code Playgroud)

因此,您确实可以定义很多不同的甚至非常强大的树转换......只是不是问题所暗示的完全通用的高阶类型。希望其中之一能为您指明前进的方向。好的,祝你好运!

Playground 代码链接