Как выполнить проверку Type Guard для индексированного свойства

У меня есть тип вложенногоObj, который использует индексированную подпись следующим образом:

type nestedObj = {[key: string]: nestedObj} | {[key: string]: number}

Как мне создать защиту типа для этого типа вложенного объекта?

  const isNestedObj = (obj: any): obj is nestedObj => {
      if (obj === null || obj === undefined)
         return false;

      ??????
      
      return true;
  }

========= РЕДАКТИРОВАНИЕ ===========

Извините, вот дополнительная информация. Вложенный Obj имеет следующее

const testObj1 = { a: { b: { c: 42 } } };
const testObj2 = { ab: { bc: { cd: { d: 2 } } } };
const testObj3 = { xy: 3};

У объекта должен быть только ОДИН ключ.
Нам действительно также необходимо проверить, является ли obj ненулевым объектом.

Я еще раз попробовал и придумал что-то вроде этого

const isNestedObj = (obj: any): obj is nestedObj => {
    // terminating conditions
    if (typeof obj !== 'object' || obj === null || obj === undefined)
        return false
    
    // if number of keys is not exactly 1
    const objKeys = Object.keys(obj);
    if (objKeys.length !== 1)
        return false
    
    // if the next nested object is a number, return true
    if (typeof obj[objKeys[0]] === 'number')
        return true

    else return isNestedObj(obj[objKeys[0]]);
}

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

Ваш тип nestedObj (обычно вы хотите, чтобы имена псевдонимов типов начинались с заглавной буквы) является бесконечно рекурсивным, что означает, что любой объект, присвоенный ему, действителен. Таким образом, не существует реальных критериев для типа охранника.

Behemoth 24.07.2024 12:16

Вы можете проверить, не является ли объект числом, а затем проверить, есть ли внутри объекта ключи.

Zxeenu 24.07.2024 12:32

@Behemoth, но разве что-то вроде {'isConnected': true} не должно возвращать значение false, поскольку это ключ с логическим значением?

balakuz 24.07.2024 12:47

Вы уверены, что хотите {[key: string]: nestedObj} | {[key: string]: number}, а не {[key: string]: nestedOb | number}?

Bergi 24.07.2024 14:51

Соответствует ли такой подход вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?

jcalz 24.07.2024 16:27

(см. предыдущий комментарий) Где в этом вопросе вообще будет иметь значение «количество ключей равно одному»? Это не имеет никакого отношения к типу. Вам нужно написать это в вопросе устно, если это является обязательным.

jcalz 24.07.2024 16:29

@jcalz, ты прав, извини, мне следует добавить это. У объекта должен быть только один ключ. Хотя у меня было ощущение, что тип, который я реализовал {[key: string]: nestedObj}, уже предполагает, что всегда есть только один ключ, но теперь я начинаю думать, что это не так.

balakuz 24.07.2024 16:38

@ Берги, глядя на твое предложение, я думаю, может быть, ты прав, {[key: string]: nestedOb | number} больше похоже на то, что мне нужно. Как ни странно, я протестировал свое отредактированное решение, изменив тип с того, что у меня было раньше, на тот, который вы предложили здесь, и он по-прежнему работает так же?

balakuz 24.07.2024 16:49

Не существует типа TS, означающего «ровно один ключ». Вы используете индексную подпись, что совсем этого не означает. Как нам действовать? Кто-то может легко написать функцию, которая гарантирует то, что вы хотите, но использовать ее в качестве защиты типа проблематично, когда тип на самом деле не существует в TS. Почему вас волнует именно один ключ? Это странное требование.

jcalz 24.07.2024 16:52

@jcalz спасибо за информацию, это определенно поможет мне лучше понять, как это работает. Я просмотрел ваш ответ, и логика определенно имеет для меня смысл. Если вы опубликуете свой ответ, я смогу его принять. Что касается ровно одного ключа, к сожалению, это было частью требования к заданию.

balakuz 24.07.2024 18:05
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой 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 для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
10
53
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Вы можете искать объекты, пересекая ключи объекта:

type NestedObj = {[key: string]: NestedObj} | {[key: string]: number}

const isNestedObj = (obj: any): obj is NestedObj => {
    if (obj === null || obj === undefined) {
        return false;
    }
    return Object.keys(obj).find(k => typeof obj[k] === "object") !== undefined;
}

function test(obj: any) {
  if (isNestedObj(obj)) {
    type t = typeof obj['something']; // NestedObj | number
  } else {
    type t = typeof obj['something']; // any
  }
}

Возможно, было бы лучше найти, когда он не вложен:

type NotnestedObj = {[key: string]: number};
type NestedObj = {[key: string]: NestedObj} | NotnestedObj;

const isNotNestedObj = (obj: any): obj is NotnestedObj => {
    if (obj === null || obj === undefined) {
        return false;
    }
    const keys = Object.keys(obj);
    const allKeysAreNumbers = keys.filter(k => typeof obj[k] === "number").length === keys.length;
    return allKeysAreNumbers;
}

function test(obj: any) {
  if (isNotNestedObj(obj)) {
    type t = typeof obj['something']; // number
  } else {
    type t = typeof obj['something']; // any
  }
}
k => typeof obj[k] === "object" не выглядит достаточным условием для NestedObj свойств. И зачем проверять только один? А зачем отвергать пустые объекты?
Bergi 24.07.2024 14:52

Извините, что вы подразумеваете под why check only one? Только один ключ? Если у одного из ключей есть вложенный объект, вы получаете NestedObj. Это должно работать для пустых объектов, потому что typeof {} — это "object".

Remo H. Jansen 24.07.2024 15:13

Добавлена ​​новая версия, но предоставьте более подробную информацию, чтобы мы могли вам помочь.

Remo H. Jansen 24.07.2024 15:24

добавил еще немного деталей, надеюсь, это поможет

balakuz 24.07.2024 16:33

Ваше новое решение мне нравится :) Вы можете опубликовать свой ответ.

Remo H. Jansen 24.07.2024 17:23

@RemoH.Jansen {} можно присвоить типу NestedObject, но он отклоняется вашей функцией isNestedObj. {x: 1} можно назначить NestedObject, но он отклоняется. { x: {}, y: false } не может быть назначен NestedObject, но будет принят вашей «охраной».

Bergi 24.07.2024 19:34
Ответ принят как подходящий

Тип

type NestedObj = { [key: string]: NestedObj } | { [key: string]: number }

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

let n: NestedObj;

n = {}; // okay
n = { a: 0, b: 1, c: 2 }; // okay
n = { a: {}, b: {}, c: {} }; // okay
n = { a: { d: 0 }, b: { e: 1 }, c: { f: {} } }; // okay

n = { a: 0, b: 1, c: "abc" }; // error, string is not allowed
n = { a: 0, b: 1, c: {} }; // error, mix of numbers and objects
n = { a: { d: 0 }, b: { e: 1 }, c: { f: { g: "abc" } } }; // error, nested string

Обратите внимание, что заявленное требование о том, что «объект должен иметь только ОДИН ключ», не соблюдается этим типом.

Таким образом, либо нам нужно принять ваш тип по номиналу и написать для него функцию защиты типа, либо нам нужно написать функцию, которая проверяет ваше требование, но не рассматривает ее как функцию защиты типа для этого типа. Я собираюсь сделать первое.

Требование, чтобы объект содержал ровно один ключ, нелегко представить в TypeScript. Если для этого нет чрезвычайно важного варианта использования, было бы лучше ослабить требования или изменить структуру данных на что-то осуществимое, например [string, ...string[], number], и написать ["a", "b", "c", 0] вместо {a:{b:{c:0}}}.


Опять же, я собираюсь написать защитную функцию специального типа, которая определяет, является ли ввод допустимым NestedObj, не заботясь о количестве ключей. Вот один из подходов:

const isNestedObj = (obj: any): obj is NestedObj => {
  if (!obj) return false;
  if (typeof obj !== "object") return false;
  const values = Object.values(obj);
  if (values.every(v => typeof v === "number")) return true;
  return values.every(isNestedObj);
}

Здесь мы сначала убеждаемся, что obj является ненулевым объектом. Затем мы используем Object.values() , чтобы получить все значения свойств объекта. Если каждое свойство является числом, то тест пройден. В противном случае мы проверим, является ли каждое свойство NestedObj.

Это обеспечивает согласованное поведение с приведенными выше тестами:

console.info(isNestedObj({})) // true
console.info(isNestedObj({ a: 0, b: 1, c: 2 })) // true
console.info(isNestedObj({ a: {}, b: {}, c: {} })) // true
console.info(isNestedObj({ a: { d: 0 }, b: { e: 1 }, c: { f: {} } })) // true

console.info(isNestedObj({ a: 0, b: 1, c: "abc" })) // false
console.info(isNestedObj({ a: 0, b: 1, c: {} })) // false
console.info(isNestedObj({ a: { d: 0 }, b: { e: 1 }, c: { f: { g: "abc" } } })); // false

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

const o: NestedObj = { a: {} };
o.a = o;
console.info(isNestedObj(o)) // 💥 TOO MUCH RECURSION;

Если вам нужно это учитывать, вам придется усложнить isNestedObj(), чтобы отслеживать объекты, которые он уже видел, чтобы он мог выйти раньше, если войдет в цикл. Я не буду этого делать здесь.


Обратите внимание: если вы попытаетесь применить правило «ровно один ключ» в функции защиты типа, сохраняя при этом тип тем же, вы можете получить странное поведение:

const isNestedObj = (obj: any): obj is NestedObj => {
  if (!obj) return false;
  if (typeof obj !== "object") return false;
  const values = Object.values(obj);
  if (values.length !== 1) return false; // exactly one key
  if (values.every(v => typeof v === "number")) return true;
  return values.every(isNestedObj);
}

const o = Math.random() < 0.99 ? { a: 1, b: 2 } : "abc";
if (!isNestedObj(o)) {
  o // "abc"
  console.info(o.toUpperCase()) // 99% chance of a runtime error
}

Значение {a: 1, b: 2} является совершенно допустимым NestedObj в зависимости от типа. Но isNestedObject({a: 1, b: 2}) теперь возвращается false. Если isNestedObject является функцией защиты типа, то TypeScript понимает, что это означает, что !isNestedObject(o) подразумевает, что o не является NestedObj. Итак, в приведенном выше примере TypeScript неправильно заключит, что o должно быть "abc".

Есть способы, которыми вы можете попытаться обойти эту проблему, сделав функцию защиты типа «односторонней», как описано в microsoft/TypeScript#15048 , или вы можете попытаться бороться с системой типов, чтобы придумать тип, который принимает объекты только с одним свойством, как описано в машинописный текст ограничивает количество свойств объекта. Но это усложняет ситуацию, и я буду считать, что это выходит за рамки рассмотрения.

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

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

Похожие вопросы

Angular 18 @для возврата ОШИБКИ TypeError: newCollection[Symbol.iterator] не является функцией
Сохранение информации о типе при обеспечении связи типов между членами кортежа при использовании в параметре функции сопоставленного типа
Как упростить это троичное выражение?
Ошибка Angular 18: TS-992003: нет подходящего токена внедрения для параметра MovieService класса MovielistComponent
Как избежать .then или повторного ожидания при объединении обещаний в TypeScript?
Получен статус ответа [FAILED] от пользовательского ресурса. Возвращено сообщение: Команда умерла с <Signals.SIGKILL: 9>
Сохранение типов массива массивов в TypeScript
Использование дженериков для сопоставления одного строкового литерала в Typescript
Обработка данных формы и регистрация пользователей в стеке MERN: проблемы с FormData и проверкой
Получение ошибки NEXT_REDIRECT при использовании перенаправления из следующего/навигации в серверной функции в Next.js