Рекурсивное разрешение объекта с безопасным типом доступа к свойствам в два шага

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

import {get} from 'lodash';

const obj = {
  foo: 'foo',
  bar: {
    a: 'Hello',
    b: {c: 'World'}
  }
};

function factory(namespace?: string) {
  return function getter(key: string) {
    return get(obj, [namespace, key].filter((part) => part != null).join('.'));
  };
}

const getter = factory('bar');
getter('b.c'); // 'World'

Точечная нотация указывает на доступ к вложенному свойству. Он может присутствовать как в namespace, так и в key.

На данный момент я узнал, что могу набирать namespace с помощью этой утилиты:

type NestedKeyOf<ObjectType extends object> =
  {
    [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
      ? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
      : `${Key}`
  }[keyof ObjectType & (string | number)];

Использование: namespace?: NestedKeyOf<typeof obj>.

Однако я изо всех сил пытаюсь придумать динамический тип, который автоматически назначается key.

Двумя дополнительными требованиями будут:

  1. Пространство имен должно когда-либо разрешаться только в объект (без конечных строк).
  2. Геттер всегда должен разрешать окончательную строку (без объектов).

Можно предположить, что в объекте присутствуют только объекты и строки, больше ничего.

// Test cases

// Should pass
factory()('foo')
factory('bar')('a')
factory('bar')('b.c')

// Invalid property access
// @ts-expect-error 
factory('baz')
// @ts-expect-error
factory('bar')('d')

// Only partial namespaces are allowed
// @ts-expect-error 
factory('foo')

// Getter calls need to resolve to a leaf string
// @ts-expect-error
factory('bar')('b')

Любая помощь могла бы быть полезна! Большое спасибо!

Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
3
0
60
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я не могу поверить, что эта мерзость действительно работает:

function factory<NestedKey extends NestedKeyOf<typeof obj>>(namespace?: NestedKey) {
  return function getter<
    TargetKey extends 
      (NestedKey extends undefined
        ? NestedKeyOf<typeof obj>
        : NestedKeyOf<Get<typeof obj, NestedKey>>)
  >(key: TargetKey): NestedKey extends undefined ? Get<typeof obj, TargetKey> : Get<typeof obj, `${NestedKey}.${TargetKey}`> {
    return get(obj, [namespace, key].filter((part) => part != null).join('.'));
  };
}

Позвольте мне объяснить. Прежде всего, у меня было много проблем с вашей реализацией NestedKeyOf :(

Мне пришлось переписать его, потому что он сообщил, что создание экземпляра типа было слишком глубоким, так что вот моя версия:

type NestedKeyOf<O> = O extends object ? {
    [K in keyof O]: `${K & string}` | `${K & string}.${NestedKeyOf<O[K]>}`;
}[keyof O] : never;

Он делает то же самое; просто написано по другому. Затем нам нужно иметь возможность «глубоко получить» свойство (имитируя функцию get lodash):

type Get<O, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof O ? Get<O[Key], Rest> : never
    : P extends keyof O ? O[P] : never;

Дополнительные extends keyof O предназначены для предотвращения ошибок при недопустимом доступе к свойству.

И, наконец, чудовище, которое я показал выше.

Нам нужно хранить то, что есть namespace, поэтому мы используем дженерик. С этим общим названием NestedKey мы теперь можем использовать его в определении getter.

getter также принимает вложенный ключ. Однако его тип отличается, когда namespace не указан.

Вот почему NestedKey extends undefined есть. Если его нет, то key должен быть вложенным ключом исходного объекта. В противном случае это вложенный ключ значения, на которое указывает namespace, используя Get.

Наконец, в возвращаемом значении мы делаем то же самое. Если NestedKey отсутствует, то мы глубоко получаем целевой ключ, в противном случае мы глубоко получаем вложенный ключ и целевой ключ.

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

Утверждение as const предназначено для проверки того, что на самом деле глубокое получение правильного значения.


Обновление в соответствии с новыми требованиями. Нам понадобятся некоторые новые типы, чтобы сказать нам, какие ключи являются объектами, а какие строками:

type GetObjectKeys<O, K extends string> = {
  [P in K]: Get<O, P> extends string ? never : P;
}[K];

type GetStringKeys<O, K extends string> = {
  [P in K]: Get<O, P> extends string ? P : never;
}[K];

Они принимают тип объекта и некоторые потенциальные ключи. Он проверяет тип каждого ключа. В случае объектов, если это строка, это never, иначе P. Мы используем never, потому что ниже мы получим объединение всех оставшихся ключей с [K], а T | never упрощается до T.

Затем из-за того, что namespace является необязательным, возникают некоторые трудности. Ошибка с моей стороны заключалась в том, что я полагал, что если namespace не будет указано, NestedKey будет неопределенным (поэтому предыдущий ответ на самом деле неверен). Это исправляется позже.

Чтобы обратиться к необязательному namespace, мы делаем параметр исходной factory функции namespaceнет необязательным и переименовываем его в _factory (внутренний/частный). Затем мы создаем новую функцию factory, которая вызывает ее так:

function factory<NestedKey extends GetObjectKeys<typeof obj, NestedKeyOf<typeof obj>>>(namespace?: NestedKey) {
  return _factory<
    { __private: typeof obj },
    GetObjectKeys<typeof obj, NestedKeyOf<typeof obj>> extends NestedKey ? "__private" : `__private.${NestedKey}`
  //@ts-ignore Unfortunately I don't think there is a good way to prevent this error
  >({ __private: obj }, namespace ? `__private.${namespace}` : "__private");
}

Он ожидает любые ключи объекта, но если пространство имен не было предоставлено, по умолчанию будет создано значение __private, потому что мы заключаем целевой объект в другой объект со свойством __private, чтобы обойти тот факт, что пространство имен является необязательным. Думайте об этом как о делегате.

Теперь для модифицированной функции _factory:

function _factory<Obj extends unknown, NestedKey extends NestedKeyOf<Obj>>(obj: Obj, namespace: NestedKey) {
  return function getter<
    TargetKey extends GetStringKeys<Get<Obj, NestedKey>, NestedKeyOf<Get<Obj, NestedKey>>>
  >(key: TargetKey): Get<Obj, `${NestedKey}.${TargetKey}`> {
    return get(obj, [namespace, key].filter((part) => part != null).join('.'));
  };
}

Эта функция в основном такая же, как оригинал, за исключением того, что теперь она ожидает только ключи, которые приводят к строкам. Часть, которая обрабатывает необязательную namespace, перемещена в новую функцию factory.

Я мог бы выбрать лучшие имена для factory и _factory, чтобы избежать путаницы, но, надеюсь, вы достаточно хорошо меня поняли.

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

Кстати, это переработано до ****; так всегда бывает, когда вы начинаете заставлять типы вести себя как настоящий код ?

Вау, это невероятно — большое спасибо за ваш суперполезный ответ @kellys! Я поиграл с вашим решением, и единственный вызов, который, как я ожидаю, сработает, но в настоящее время является ошибкой типа: factory()('foo'). Каким-то образом namespace кажется по умолчанию bar. Помимо этого, для моего конкретного случая использования я обнаружил, что было бы здорово удовлетворить еще два требования: 1. Пространство имен должно когда-либо разрешаться только в объект (без конечных строк). 2. Геттер всегда должен разрешать окончательную строку (без объектов). Я добавил их к вопросу выше.

amann 21.03.2022 13:39

Я добавил несколько тестовых случаев к моему вопросу выше. Если у вас появятся какие-либо дополнительные мысли, это было бы невероятно! Большое спасибо!

amann 21.03.2022 13:43

Хорошо, я посмотрю, когда смогу

kellys 21.03.2022 14:18

@amann Я полагаю, что удовлетворил ваши требования в своем отредактированном ответе

kellys 21.03.2022 15:19

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

amann 21.03.2022 22:17

В итоге заработало: twitter.com/jamannnnnn/status/1509897249853169666. Еще раз большое спасибо @kellys!

amann 04.04.2022 10:05

Другие вопросы по теме