Рекурсивно преобразовать все листья дерева объектов в машинописи - PullRequest
0 голосов
/ 16 апреля 2020

Учитывая простое дерево объектов, которое содержит либо значения своего собственного типа, либо типа, который необходимо преобразовать:

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

Я хочу определить функцию, которая рекурсивно преобразует все листья в другой тип, например this:

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

Playground Link

Кажется, что машинопись все еще думает, что если значение поля не лист, то это может быть нечто иное, чем поддерево.

1 Ответ

1 голос
/ 17 апреля 2020

В дальнейшем я буду беспокоиться только о типах, а не о реализации. Все функции будут просто declare d, как будто фактические реализации находятся в некоторой библиотеке JS, и это их файлы объявлений .

Кроме того, ваши isLeaf функции, вероятно, должны быть напечатаны как определяемые пользователем типы защиты , возвращаемый тип которых - предикат типа вместо просто boolean. Сигнатура функции isLeaf: (value: S | Tree<S>) => value is S аналогична сигнатуре, которая возвращает boolean, за исключением того, что компилятор фактически поймет, что в if (isLeaf(x)) { x } else { x } x в блоке true будет S и x в false блок будет Tree<S>.

Хорошо, вот так:


Тип Tree<X> является слишком общим для отслеживания определенных c типов ключей и значений. Все, что компилятор знает о значении типа, скажем, Tree<string>, - это то, что это тип объекта, свойства которого имеют тип string или Tree<string>. Как только вы это сделаете, скажем так:

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

Вы выбросили все детали о конкретной структуре, а также все детали о любых c подтипах 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

Но вы хотите что-то значительно более амбициозное здесь ; кажется, что вы хотите иметь карту преобразования листа не только от определенного c типа X к конкретному c типу Y, но вы хотите указать некоторую функцию общего типа, такую ​​как type F<T extends X> = ... и сопоставьте лист от типа Z extends X до F<Z>.

В вашем примере ваш тип ввода похож на Wrapped<any>, а ваша функция типа вывода будет выглядеть как type F<T extends Wrapped<any> = T["value"];

К сожалению, этот более общий тип преобразования не может быть выражен в TypeScript , Вы не можете иметь функцию типа, например type TransformType<T extends Tree<X>, X, F> = ..., где F сама является функцией типа, которая принимает параметр. Это потребовало бы, чтобы язык поддерживал то, что известно как «типы с более высоким родом». Для этого существует давний запрос на открытую функцию по адресу microsoft / TypeScript # 1213 , и хотя было бы удивительно иметь их, не похоже, что это произойдет в обозримом будущем.


То, что вы можете сделать, - это представить конкретные типы преобразований конечных типов и реализовать для них определенные c версии 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);

Или, как упоминалось в ваших комментариях, вы можете сделать обратное с определенным generi c интерфейсом / классом, как, скажем, Wrapper. .. сопоставление заключается в преобразовании Z extends X в 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 пар, которые позволяют каждому узлу быть проверены на различные более специфичные c преобразования. Так, например, каждый раз, когда вы находите значение "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

Таким образом, существует множество различных и даже довольно мощных преобразований дерева, которые вы можете определить. .. только не полностью обобщенный c тип высшего порядка, подразумеваемый этим вопросом. Надеюсь, один из них даст вам путь вперед. Хорошо, удачи!

Детская площадка ссылка на код

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...