У меня есть тип вложенного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]]);
}
Кажется, у меня работает, но не уверен, что это оптимальное решение.
Вы можете проверить, не является ли объект числом, а затем проверить, есть ли внутри объекта ключи.
@Behemoth, но разве что-то вроде {'isConnected': true} не должно возвращать значение false, поскольку это ключ с логическим значением?
Вы уверены, что хотите {[key: string]: nestedObj} | {[key: string]: number}
, а не {[key: string]: nestedOb | number}
?
Соответствует ли такой подход вашим потребностям? Если да, то я напишу ответ с объяснением; если нет, то что мне не хватает?
(см. предыдущий комментарий) Где в этом вопросе вообще будет иметь значение «количество ключей равно одному»? Это не имеет никакого отношения к типу. Вам нужно написать это в вопросе устно, если это является обязательным.
@jcalz, ты прав, извини, мне следует добавить это. У объекта должен быть только один ключ. Хотя у меня было ощущение, что тип, который я реализовал {[key: string]: nestedObj}
, уже предполагает, что всегда есть только один ключ, но теперь я начинаю думать, что это не так.
@ Берги, глядя на твое предложение, я думаю, может быть, ты прав, {[key: string]: nestedOb | number}
больше похоже на то, что мне нужно. Как ни странно, я протестировал свое отредактированное решение, изменив тип с того, что у меня было раньше, на тот, который вы предложили здесь, и он по-прежнему работает так же?
Не существует типа TS, означающего «ровно один ключ». Вы используете индексную подпись, что совсем этого не означает. Как нам действовать? Кто-то может легко написать функцию, которая гарантирует то, что вы хотите, но использовать ее в качестве защиты типа проблематично, когда тип на самом деле не существует в TS. Почему вас волнует именно один ключ? Это странное требование.
@jcalz спасибо за информацию, это определенно поможет мне лучше понять, как это работает. Я просмотрел ваш ответ, и логика определенно имеет для меня смысл. Если вы опубликуете свой ответ, я смогу его принять. Что касается ровно одного ключа, к сожалению, это было частью требования к заданию.
Вы можете искать объекты, пересекая ключи объекта:
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
свойств. И зачем проверять только один? А зачем отвергать пустые объекты?
Извините, что вы подразумеваете под why check only one
? Только один ключ? Если у одного из ключей есть вложенный объект, вы получаете NestedObj
. Это должно работать для пустых объектов, потому что typeof {}
— это "object"
.
Добавлена новая версия, но предоставьте более подробную информацию, чтобы мы могли вам помочь.
добавил еще немного деталей, надеюсь, это поможет
Ваше новое решение мне нравится :) Вы можете опубликовать свой ответ.
@RemoH.Jansen {}
можно присвоить типу NestedObject
, но он отклоняется вашей функцией isNestedObj
. {x: 1}
можно назначить NestedObject
, но он отклоняется. { x: {}, y: false }
не может быть назначен NestedObject
, но будет принят вашей «охраной».
Тип
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 , или вы можете попытаться бороться с системой типов, чтобы придумать тип, который принимает объекты только с одним свойством, как описано в машинописный текст ограничивает количество свойств объекта. Но это усложняет ситуацию, и я буду считать, что это выходит за рамки рассмотрения.
Ваш тип
nestedObj
(обычно вы хотите, чтобы имена псевдонимов типов начинались с заглавной буквы) является бесконечно рекурсивным, что означает, что любой объект, присвоенный ему, действителен. Таким образом, не существует реальных критериев для типа охранника.