Typescript: глубокий ключ вложенного объекта - PullRequest
0 голосов
/ 17 октября 2019

Итак, я хотел бы найти способ получить все ключи вложенного объекта.

У меня есть универсальный тип, который принимает тип в параметре. Моя цель - получить все ключи данного типа.

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

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?

1 Ответ

4 голосов
/ 17 октября 2019

Как уже упоминалось, в настоящее время невозможно объединить строковые литералы на уровне типа. Были предложения, которые могли бы позволить это, например, предложение разрешить увеличение ключей при отображаемых типах и предложение проверить строковые литералы с помощью регулярного выражения , но пока это невозможно.

Вместо того, чтобы представлять пути как пунктирные строки, вы можете представлять их как кортежи строковых литералов. Таким образом, "a" становится ["a"], а "nest.c" становится ["nest", "c"]. Во время выполнения достаточно просто выполнить преобразование между этими типами с помощью методов split() и join().


Так что вы можете захотеть что-то вроде Paths<T>, которое возвращает объединение всех путей для данного типаT, или, возможно, Leaves<T>, то есть только те элементы Paths<T>, которые указывают на сами необъектные типы. Нет встроенной поддержки для такого типа;в библиотеке ts-toolbelt есть , но, поскольку я не могу использовать эту библиотеку на 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]-?: Cons<K, Paths<T[K], Prev[D]>> | Paths<T[K], Prev[D]> }[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]. Реализация использует кортежей отдыха / распространения . Это понадобится нам для построения путей.

Тип Prev - это длинный кортеж, который можно использовать для получения предыдущего числа (до максимального значения). Так что Prev[10] равно 9, а Prev[1] равно 0. Нам понадобится это, чтобы ограничить рекурсию, когда мы углубимся в дерево объектов.

Наконец, Paths<T, D> и Leaves<T, D> реализуются путем перехода к каждому типу объекта T и сбора ключей, иCons их на Paths и Leaves свойств этих ключей. Разница между ними заключается в том, что Paths также напрямую включает подпути в объединении. По умолчанию параметр глубины D равен 10, и на каждом шаге вниз мы уменьшаем D на единицу до тех пор, пока не попытаемся пройти 0, после чего мы прекращаем повторение.


Хорошо, давайте проверим это:

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"]

Это заставит компилятор перестать смотреть на глубину 3, поэтому все ваши пути имеют длину не более 3.


Итак, это работает. Вполне вероятно, что ts-toolbelt или какая-то другая реализация может позаботиться о том, чтобы у компилятора не было сердечного приступа. Так что я не обязательно сказал бы, что вы должны использовать это в своем производственном коде без значительного тестирования.

Но в любом случае вот ваш желаемый тип, при условии, что вы имеете и хотите Paths:

type MyGenericType<T extends object> = {
    keys: Array<Paths<T>>;
};

const test: MyGenericType<NestedObjectType> = {
    keys: [['a'], ['nest', 'c']]
}

Надеюсь, это поможет;удачи!

Ссылка на код

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