Как правильно определить общий тип для вложенного объекта

#typescript

#typescript

Вопрос:

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

 interface Nested {
    id: number;
    children?: Nested[];
}

interface Props<T> {
    elements: T[];
    childProp: string;
    idProp: string;
}

function recursive<T>(element: T, childProp: string, idProp: string) {
    console.log(element[idProp], childProp, element[childProp]);
    if (element[childProp]) {
        element[childProp].forEach((el: T) => {
            recursive<T>(el, childProp, idProp);
        });
    }
}

function test<T>(props: Props<T>) {
    props.elements.forEach((element) => {
        recursive<T>(element, props.childProp, props.idProp);
    });
}

const nested: Nested[] = [
    {
        id: 1,
        children: [
            {
                id: 2,
                children: [
                    {
                        id: 3
                    }
                ]
            },
            {
                id: 4,
                children: [

                ]
            },
        ]
    },
    {
        id: 5
    }
]

test<Nested>({
    elements: nested,
    childProp: 'children',
    idProp: 'id'
});
  

Технически код работает, но в recursive функции я получаю неявную ошибку any. Вложенные объекты будут иметь некоторое поле, которое будет указывать его идентификатор (не всегда id, может быть CategoryID или что-то еще) и необязательное поле, содержащее массив объектов с одинаковой структурой (не всегда дочерних).

Проблемы заключаются в

 function recursive<T>(element: T, childProp: string, idProp: string) {
    console.log(element[idProp], childProp, element[childProp]);
    if (element[childProp]) {
        element[childProp].forEach((el: T) => {
            recursive<T>(el, childProp, idProp);
        });
    }
}
  

с element[idProp] помощью и element[childProp]

Ответ №1:

В вашем первоначальном определении recursive параметр общего типа T совершенно не ограничен и может быть любым. Кроме того, на уровне типа, childProp и idProp не может реально способствовать наборам с таким общим типом ( string ), когда мы хотим, чтобы их значения имели значение. т. Е. Нам нужно больше литеральных типов для них.

Попробуйте следующее, которое пытается дать более общее определение формы объектов, которые мы ищем:

 type MyElement<CKey extends string, IKey extends string>
  = { [K in CKey]?: MyElement<CKey, IKey>[] } amp; { [K in IKey]: number } amp; Record<string, any>;
  

{ [K in CKey]?: MyElement<CKey, IKey>[] } : описывает объект со свойствами, названными by CKey , как необязательный массив дочерних элементов, которые используют один и тот же CKey и IKey .

{ [K in IKey]: number } : описывает объект со свойствами, названными через IKey , чтобы быть number .

Record<string, unknown> : описывает объект с дополнительными свойствами неизвестных типов. Мы используем unknown такие, чтобы их использование давало лучшую ошибку, чем any та, которая молча позволит вам выйти из системы типов. Это используется, чтобы сказать, что дополнительные свойства объектов в порядке.

Затем мы объединяем оба с amp; , чтобы сказать, что объект должен удовлетворять всем ограничениям. Взгляните на пример:

 const t: MyElement<'children', 'testId'> = { testId: 30, children: [{ testId: 40 }] };
  

Теперь мы можем обновить сигнатуру recursive , чтобы использовать новые ограничения:

 function recursive<CKey extends string, IKey extends string>(element: MyElement<CKey, IKey>, childProp: CKey, idProp: IKey) {
  console.log(element[idProp], childProp, element[childProp]);
  if (element[childProp]) {
    element[childProp].forEach(el => {
      recursive(el, childProp, idProp);
    });
  }
}
  

И, конечно, некоторые тесты, чтобы убедиться, что все выполняется с проверкой типов, как ожидалось:

 recursive({ testId: 10 }, 'children', 'testId');
recursive({ testId: 10, children: [], anyprop: 'something', date: new Date() }, 'children', 'testId');

// Expected error, children elements must have a `testId`
recursive({ testId: 10, children: [{}] }, 'children', 'testId');
recursive({ testId: 10, children: [{ testId: 13 }] }, 'children', 'testId');

recursive({ testId: 10, children: [{ testId: 13, children: [{ testId: 15 }] }] }, 'children', 'testId');
// Expected error, the deepest `children` must be an array our these id'd elements
recursive({ testId: 10, children: [{ testId: 13, children: {} }] }, 'children', 'testId');
  

Попробуйте это на игровой <a rel="noreferrer noopener nofollow" href="https:///www.typescriptlang.org/play/#src=type MyElement
= { [K in CKey]?: MyElement[] } & { [K in IKey]: number } & Record;

const t: MyElement = { testId: 30, children: [{ testId: 40 }] };

function recursive(element: MyElement, childProp: CKey, idProp: IKey) {
console.log(element[idProp], childProp, element[childProp]);
if (element[childProp]) {
element[childProp].forEach((el) => {
recursive(el, childProp, idProp);
});
}
}

recursive({ testId: 10 }, ‘children’, ‘testId’);
recursive({ testId: 10, children: [], anyprop: ‘something’, date: new Date() }, ‘children’, ‘testId’);

// Expected error, children elements must have a `testId`
recursive({ testId: 10, children: [{}] }, ‘children’, ‘testId’);
recursive({ testId: 10, children: [{ testId: 13 }] }, ‘children’, ‘testId’);

recursive({ testId: 10, children: [{ testId: 13, children: [{ testId: 15 }] }] }, ‘children’, ‘testId’);
// Error, the deepest `children` must be an array our these id’d elements
recursive({ testId: 10, children: [{ testId: 13, children: {} }] }, ‘children’, ‘testId’);» rel=»nofollow noreferrer»>площадке!

Комментарии:

1. Спасибо, это действительно полезно, но как насчет объектов, которые имеют больше свойств: например. { children: [], id: 'something', anyprop: 'something' date: new Date() }

2. Хорошее наблюдение, обновлено, чтобы включить это Record<string, unknown> , чтобы разрешить дополнительные свойства с типом unknown , поскольку наша функция никогда их не затрагивает.

3. Еще раз спасибо, это именно то, что мне было нужно