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

2024-02-29

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

interface Tree<Leaf> {
  [key: string]: Tree<Leaf> | Leaf;
}

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

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),
    }))
  );
}

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

测试以上不起作用:

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"'.

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

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

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
          ),
    }))
  );
}

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


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'.

游乐场链接 https://www.typescriptlang.org/play/index.html?target=7#code/C4TwDgpgBAKgThCAeAMhAhgMwHxQLxQDeAsAFBRQDaA1hCAFxQDOwcAlgHYDmAuo-IlQYcUAD5Q0WANxkAvjNJlQkWHHQcmmAPZwAtgOSTMAGigBJDmACuwA1AgAPYBA4ATJqsFHspmLgIk5FQA0lCcULQgWpjmljYGfLHWtggQlME89k4u7hLCZBQUAPywBYWMFskG6ZmOzm4eBkJY2GXFquqaOvqpzSZJ8ak1vq1BFIwcEABuEHAK8mRK4NBmTEZ9-lAAFFPoADZWEPy93mJ5WACU+LgARlpaexgcCmSYVhwAxsBsWhxQrJ1tHomjAsvVck0AGJwLS6HxQaGw3xabBbMqsRD8YzotQaIG6Rg7faHRiI3RXPC4GBabFBNhrYQVBlYJBk0YXY6A7pQmG6UzKCDRf6pZG4QIUBDAKxwP4AeRuACsIF8AHToJhMNhcDhosZEWS0wpQFUm+VK1UuVhsCBMLYYiAXFW6dBgLZbGh0Uy7A4QHgU3BbcVGqiRRL0oxEn0XNrBkoAvHdSOHKDqqAAUScai+SAFQvtlFzMVDpjZ0b1RsY8a6wNSuuD9ag3uTqYzAOzheFiALyyFxc8yDZPhjDeFXL0hpHhXD+XLwYuE4osguZYoF3mi1IHz26o8AHU1GBIHAkH4iGUPr8WHArF8dFtrDc9mwPo3iUdYFdCLI5Bu3p9vr8o4JnoJ6ogA7gekCuIw+4ukeoEcrAZ5BJK0p-BBcEQK4KpNhA66KKQf5fD8fzvBhYCgXWUDkVBxyCLBh6zKB7KcsBPT0ZBTF+PyPYxDRWEjMhEoQFKMpAdW7EQFRFD8a4C7ifi8lJu+DHwae4hNKpXHYNg-qvj6YQaMA6gfIKMRaXAZRrj+BEXkZUBaIq+BQGRB5IFenBcKiQbaFojBBhQNzoHAEwQGBUAWVsABEQUAF5RfOZQGueWhgaF4WRVFuj3AltJLi8hHvMRgEABbqK4jwAELoLFymMDFNUJUJUCoWJrCHPhrxFQBfxlW4jwALL3HVUBZTln5lK1fztXhNlkH1FUQENWhbI5CoqheYFrlAAD0O1QLKwQbgtVU1atioqr5KpBXA217bAADKABMADML0AJyMAACjCR6gFAADkN0A1ArhaDaUAcFowBZPSMOAQKgOTDMcAAyqZBAA

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


接下来,我将只关心打字而不是实现。所有功能都将是declared,就好像实际的实现在某个 JS 库中,这些是它们的声明文件 https://www.typescriptlang.org/docs/handbook/declaration-files/by-example.html#global-functions.

另外,你的isLeaf函数可能应该键入为用户定义的类型保护 https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards其返回类型是类型谓词而不仅仅是boolean。函数签名isLeaf: (value: S | Tree<S>) => value is S与返回的类似boolean,除了编译器实际上会理解if (isLeaf(x)) { x } else { x }, the x在真实的块中将是Sx在错误块中将是Tree<S>.

好的,这里是:


方式Tree<X>过于笼统,无法跟踪特定的键和值类型。所有编译器都知道该类型的值,例如,Tree<string>,它是一个对象类型,其属性是以下任一类型string or Tree<string>。一旦你这样做了,就说:

const x: Tree<string> = { a: "", b: { c: "", d: { e: "" } } };

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

x.a.toUpperCase(); // error
x.z; // no error 

如果您关心的只是提出一个类型转换来维护嵌套键结构并将某些子类型转换为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>;

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

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

但你想要一些更雄心勃勃的东西;看来你想要的叶子变换图不仅仅是来自特定类型X到特定类型Y,但您想指定一些通用类型函数如 type F<T extends X> = ...并从类型映射叶子Z extends X to F<Z>.

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

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


你什么can要做的是设想特定类型的叶类型转换并实现特定版本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>;

然后使用它:

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);

或者,正如您的评论中提到的,您可能想做相反的事情特别的通用接口/类,比如,Wrapper...映射是要转换Z extends X into Wrapper<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>;

并使用它:

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"您输出的树中的值number任何时候你找到一个"baz"您输出的树中的值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>>;

然后你使用它:

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

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

Playground 代码链接 https://www.typescriptlang.org/play/index.html#code/C4TwDgpgBAKgThCAeGA+KBeKBvAsAKCiKgG0BrALigGdg4BLAOwHMBdKmKAH1gWTQDcBAL4ECAYwD2jWlAAeHPkloMW6LNigBDKgCJdAGigAjKpvF7DUACZmoES1GFOnQ-HIB0Wj8EkBVMEg4AGEtaggACgBKASgAejj7ODhJOAJPAC9YhKhGSSSUuCgxfFBIXi0ZADNUgFt4RBR7OWAIRmtqXkaADVQjbqMATXUcUgBpKCYoMggQSSrYdgJiWBIx1mbW9s7uqAB+KEGoCmXiGDWNiBa2jq7kXv2K6rqG-gv+ofQTwjOLt2E3NYIOIADZaBBQKoAV0Y4mA9GkUDolWoNTg9SUA0ORhguyuW1uryQvVQEVOREkxgAVhwBuSkXAUWjalQIgA3LQgqEOKDdKKYdCDAz0+jUAAyEC0VVZHK5PN2PCJvX5GHQsu5kx2BCiiiZLyUuI+h1Qbgk0lkwAAjJhRjooLoAIJWUyjCz24LBKy2UY83QAUQDuhczgBZpkwCRACYbcjnujXhErcKfkQIgoaHQmMwVehPCC2sxgAALZMrdk6qBszUZ1TMAVI8AQeaVzAYLC6FRZ3TatzASNebKJRhQ2rGCBpUr94wecSD3IjscTvseafWDwQOfD0fjkplaDwPXxvgASWschQRom+JunRmcwWDw0pAACpNGNNZs2YEsU6tn5drm2XlHm6C5jnpA84wxRBT3Pc5-0vE0REBYEwQhaFYXhRFY1RfUYLPFAsSxK9ANuO9m16Mlf0pGlYDpX87yoMZS2IUUJSlGVOW5KhKhAHNKy46BRV5bVdSg15YMIxDTXwUEwk6AB1RlAnHFB0DwX8pHDOAoThVIIjAKFjBBehxAEuUOH5TRRHwGytNkAB3G0NJWGpJDMekVmMcEqEYCAnKUrQVLgCJdG8jIgzCKB7OAKIWKIYR4uiyQHN8-yoEC4LQtqSRJEizoYri5CShiqAHOjLAcOZCSzyolYHKMXR1QgKw0x4xg+KoORq0yoIkF49RczfWhKnEJsFl6nd8BiEoMLhBF3yLSprHzAAhLQMnZQS9HC3QrKcAg5qwxblvzABZXKtos+0cry-abKW9o1o2iJyo8NyV3BGb8EelaIAuyRXv7KQHO+3dGyeXCj0QTKL15EZNBIV8pnIhZv2+FZ4IAgkdkeSa4BQJHWC+CDGXEvhYax-okNslC5PQmF5uwsmoegiBKaIowSJxj973huriBo2kkrYyVpSgK7uO0Dr+Oa6tulEyHqop5SpPhmSw1kKFnPpNyPN-LyfPtXbtAK81gCSxL6RBvRbvy5Lw2TUN8E1iMoQqhlDzZzKBaIKEjDa6XOvkatOxYes92bbq23bMPmG7aa3F+57Nvd97cs+uAPGa77k-+y605B7PBLBl2y4IPclbw-6grhs7NhvUheKMXjiec8Y315r8f0xsDryAs6SAABg2A4-RaRk4SQM6jBIc51hbjriZIS0Ngxs4WeVxAztr+fWCMM70GdiuIePcUxYddpINZ1TjyMAB5BGRXPjiJfTXj+O64S7-pKq6lZdMx5+L3xECfco19mQOmSFoEAMBJA7zANPBuQEoGMhAEgM+7EqiX2sBAuoqlm5B1QMQnWv41id1RlAM67AqF91Ip0TBF8r6b3wQTJgVRxxQDvm+DhRRH77HpCQbh98165AgGyThwgSBbkXKwEoQJ6bQCOgtT25Nt67wPsg24qCYEYJftg5hXsCEdUXiAYhpJ6RC1gElDwti-7onHNQKgZ1FZ4OhjXRBMAcQsPRDo2B8Da6Hxpq7KAtRagxh8WzBBER-b0k0KLV+gcP5dWrLoO29Zo5thurlKw9iWRv1tjk-ilpIwAGYnBJXifogB7Vg5f06GFDaQZVQhxjsbJpRg8k1PaRFfidANQ2W+mEmcKV4hDgXFNYZH1vJFByMYXK+ZKglCgEAA

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 使用 XLSX.readFile 读取文件

    在 Typescript 中 执行时出现错误 无法读取未定义的属性 替换 const xlsx XLSX readFile fileName filename 是现有文件的路径 我读过 readFile https docs sheetjs
  • 比较两棵树的伪代码

    这是我遇到过几次的问题 并且不确信我使用了最有效的逻辑 例如 假设我有两棵树 一棵是文件夹结构 另一棵是该文件夹结构的内存 模型 我希望比较两棵树 并生成一棵树中存在的节点列表 而不是另一棵树中存在的节点列表 反之亦然 是否有公认的算法来处
  • 最低共同祖先算法

    所以我一直在研究实现最低共同祖先算法 我研究了许多不同的算法 主要是 Trajan 解决方案的变体或 RMQ 的变体 我正在使用非二叉树 我的树经常会在查询之间发生变化 因此预处理不一定值得 树的节点数不应超过 50 75 个 我想知道的是
  • TypeScript 源映射文件不适用于 Chrome

    我正在尝试在 Chrome 中进行 TypeScript 源调试 但遇到了两个特定且可能相关的问题 第一个是由 TypeScript WebEssentials 编译器生成的注释 该注释应该标识源映射文件的位置 如下所示 sourceMap
  • 在 TypeScript 中生成具有单个模块的声明文件

    给定以下文件夹结构 src foo ts bar ts baz ts index ts Where foo ts bar ts and baz ts每个导出一个默认类或事物 即在foo ts export default class Foo
  • Angular 5 - 谷歌未定义(谷歌地图)

    我想在我的 Angular 5 应用程序上使用谷歌地图 但遇到了一些问题 加载视图时 我在 js 控制台中收到错误 LoginComponent Host ngfactory js sm 1 ERROR ReferenceError goo
  • IE 11 的 Map(iterable) 替代方案

    不幸的是我必须支持IE11 我使用以下代码创建地图 已使用 entries 的 polyfill const map new Map Object entries array 但由于IE11不支持iterable构造函数中Map 是空的 我
  • Angular - 通用服务的提供者

    我已经为我的 HTTP API 创建了一个通用服务 并且我想为不同的端点提供此服务 而无需为每个端点创建一个类 我的服务看起来像这样 export class ApiService
  • 不要使用对象作为类型

    我收到 lint 错误 不要使用对象作为类型 当我使用对象作为类型时 示例如下 export const myFunc obj object string gt return obj toString 知道我应该为具有未知属性的对象赋予什么
  • TypeScript 通过值类型缩小可索引类型的键范围

    假设我们有一个通用的可索引类型 我如何才能仅检索其值的类型以便能够缩小到仅某些键 imagine check is a function and its second argument only allows the property a
  • TypeScript 和 Chrome 通知

    我正在构建一个 Chrome 应用程序 该应用程序是用 TypeScript Angular2 编写的 我想推送通知 这是代码 import Injectable from angular2 core Injectable export c
  • Javascript 对象属性名称

    在 C 中 可以将对象属性的名称作为字符串值获取 名称 对象 Property gt myProperty 这可以在 Javascript Typescript 中完成吗 Object Keys 是我找到的唯一东西 但它给了我所有的键 示例
  • 使用函数重载进行解构

    我正在尝试创建一个函数 该函数需要一对坐标或一个对象x and y属性并返回邻居列表 但由于某种原因 即使我检查了它的类型 我也无法解构该对象 interface Coords x number y number public getNei
  • 如何将require转化为第三方库的import语句?

    在我的打字稿项目中我使用 const program require commander const figlet require figlet const AWS require aws sdk 我想重构这些线路以通过import相反 为
  • 如何导入 nano (couchdb) - typescript

    我在节点应用程序中导入和使用 nano 时遇到问题 js 方式 来自文档 是 var nano require nano http localhost 5984 我该如何用打字稿做到这一点 I tried import as Nano fr
  • 如何在泛型方法中引用类型参数的值

    此代码无法编译 因为对 CreateItem 的调用的第一个参数不是作用域内的变量 abstract class Catalog
  • 使用 SystemJS 和 TypeScript 的 Angular 应用程序中的“意外令牌导出”

    问题 神秘的 意外的代币导出 我碰巧在 plunker 中运行的 Angular 示例中遇到此错误 其中 SystemJS 在浏览器中转换 TypeScript 代码 代码没有任何问题本地运行良好 Solution 这不是角度问题 在浏览器
  • 如何在打字稿中使用react-navigation的withNavigation?

    我正在尝试结合使用react native react navigation 和typescript 来创建一个应用程序 只有两个屏幕 HomeScreen and ConfigScreen 和一个组件 GoToConfigButton 总
  • Angular 12将json导入到ts中

    我有一个 json 文件src assets version json包含以下内容 VERSION 1 0 0 然后我将文件导入到 ts eg import as VersionInfo from src assets version js
  • 如何在 Angular httpClient 拦截器中使用异步服务

    使用Angular 4 3 1和HttpClient 我需要将异步服务的请求和响应修改为httpClient的HttpInterceptor 修改请求的示例 export class UseAsyncServiceInterceptor i

随机推荐