Введение
Все так или иначе сталкивались с функцией или методом, который принимает объект и свойства в виде строки с которым нужно что-то сделать. Пример:
updateDate(user, "date");
И когда изменяется свойства объекта (user.date
-> user.birthday
), компилятор его нормально скомпилирует и мы лишаемся возможности отловить баг на стадий разработки.
Проблема
Что в рабочих, что в пет-проектах, использую библиотеку Element plus. У него есть компонент ElTable c атрибутом date
, принимающий список объектов, для отображение и slot
принимающее компоненты ElTableColumn
, отображающее колонки, который имеет атрибут prop
, принимает в виде строки свойства поля, который нужно отобразит.
<el-table :data="tableData"> <el-table-column prop="date" label="Date" /> <el-table-column prop="name" label="Name" /> <el-table-column prop="address" label="Address" /> </el-table>
const tableData = [ { date: "2016-05-03", name: "Tom", address: "No. 189, Grove St, Los Angeles", }, { date: "2016-05-02", name: "Tom", address: "No. 189, Grove St, Los Angeles", }, ];
В начале разработки и особенно когда бизнес требование не совсем прозрачны, объекты могут меняться несколько раз в день.
Решение проблемы
Для запуска типизации, создадим простую функцию, который будет принимать обобщенный тип, где и будет происходит проверка.
export function validatePath<T>(path: ValidPath<T>): ValidPath<T> { /** * валидацию пути объекта */ return path; }
Как нетрудно догадаться, функция принимает строку и его же возвращает, что делает его практической бесплатной в виде затрат на ресурсы, а все проверки будут происходит на этапе компиляции.
Давайте опишем тип ValidPath
:
export type ValidPath<T, Prefix extends string = ''> = T extends object ? { [K in keyof T & (string | number)]: `${K}` | `${K}.${ValidPath<T[K], `${Prefix}${K}.`>}` }[keyof T & string] : never
В начале, если это объект, то он возвращает всевозможные строковые ключи, в ином случай, он ничего не возвращает. Это нужно для разрешение рекурсий.
[K in keyof T & (string | number)]
— идет перебор всех свойств, который является строкой или числом.
`${K}` | `${K}.${ValidPath<T[K], `${Prefix}${K}.`>}`;
Тут самое интересное. Он возвращает свойства в виде строки и рекурсивно вызывает себя, который возвращает все свойства объекта, но уже с префиксам.
Если, в рекурсивный тип
{ [K in keyof T & (string | number)]: `${K}` | `${K}.${ValidPath<T[K], `${Prefix}${K}.`>}`}
мы передадим
type T = { user: { name: string; age: number; }; };
для него будет получен промежуточный объект:
{ id: "id"; user: "user" | "user.name" | "user.age"; }
Далее, мы получем все свойства: [keyof T & string]
получая по итогу тип:
"id" | "user" | "user.name" | "user.age"
Запустив функцию мы получаем:
Все работает, но постойте, нам нужны лишь свойства, а туда еще и попали функции. Для исправление создадим тип, который будет исключать все функции:
type PropertiesOnly<T> = Pick< T, { [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K; }[keyof T] >;
И внедряем в наш рекурсивный тип:
export type ValidPath<T, Prefix extends string = ""> = T extends object ? { [K in keyof PropertiesOnly<T> & (string | number)]: | `${K}` | `${K}.${ValidPath<T[K], `${Prefix}${K}.`>}`; }[keyof PropertiesOnly<T> & string] : never;
Думаете на этом все? Как бы не так. Для сложных типов он выдает ошибку:
error TS2590: Expression produces a union type that is too complex to represent
Это происходит из-за того, что он не может нормально обработать объекты внутри массивов. Так что мы просто берем и исключаем их:
type IsArray<T> = T extends (infer U)[] ? true : false export type ValidPath<T, Prefix extends string = ''> = T extends object ? { [K in keyof PropertiesOnly<T> & (string | number)]: IsArray<T[K]> extends true ? never : `${K}` | `${K}.${ValidPath<T[K], `${Prefix}${K}.`>}` }[keyof PropertiesOnly<T> & string] : never
На этом все.
Итоговый код
type PropertiesOnly<T> = Pick< T, { [K in keyof T]: T[K] extends (...args: any[]) => any ? never : K }[keyof T] > type IsArray<T> = T extends (infer U)[] ? true : false export type ValidPath<T, Prefix extends string = ''> = T extends object ? { [K in keyof PropertiesOnly<T> & (string | number)]: IsArray<T[K]> extends true ? never : `${K}` | `${K}.${ValidPath<T[K], `${Prefix}${K}.`>}` }[keyof PropertiesOnly<T> & string] : never export function validatePath<T>(path: ValidPath<T>): ValidPath<T> { /** * валидацию пути объекта */ return path }
Дополнение
Тип ValidPath
, можно использовать для разных функции, что мне недавно пригодилось:
function floatHandler<T>(value: string, row: any, property: ValidPath<T>, propertyRaw: string) { /*code*/ } floatHandler<(typeof data)[0]>(value, scope.row, 'quantity', 'quantityRaw')
ссылка на оригинал статьи https://habr.com/ru/articles/861644/
Добавить комментарий