Типизация свойства объекта в виде строки

от автора

Введение

Все так или иначе сталкивались с функцией или методом, который принимает объект и свойства в виде строки с которым нужно что-то сделать. Пример:

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/