Нужна помощь с манипуляцией типов. У меня есть следующий интерфейс
interface IFormData<Inputs> {
data: Inputs;
transformData?: (data: Inputs) => <Результат преобразования данных>;
getData?: (values: Inputs or "Результат преобразования данных") => {...}
}
Обратите внимание, что transformData
может быть передан как функция, а может и не быть, это необязательный параметр.
Как реализовать так, чтобы при передаче функции transformData
мы изменяли тип в getData(values: ...)
?
Функция для интерфейса
function transformer<T>({
getData,
transformData,
data
}: IFormData<T>) {
if(getData) {
if(transformData) {
getData(transformData(data)) // что-то сделать
} else {
getData(data) // что-то сделать
}
}
}
Примеры:
-
С
transformData
:transformer({ data: { first_name: "", last_name: "string", age: 34 }, transformData: (data) => { return { fullname: data.first_name + " " + data.last_name, age: data.age } }, getData(values) { "values" должны быть { fullname: string; age: number } } })
-
Без
transformData
:transformer({ data: { first_name: "John", last_name: "Makeev", age: 34 }, getData(values) { "values" должны быть { first_name: string; last_name: string; age: number } } })
Результат – интерфейс, который помогает мне правильно задавать типы функций
Чтобы это работало, вам нужно, чтобы ваш тип IFormData
был обобщенным как в типе I
входных данных data
, так и в типе R
возвращаемого значения для transformData
:
interface IFormData<I, R> {
data: I;
transformData?: (data: I) => R,
getData?: (values: R) => any;
}
Затем ваша функция transformer
также должна быть обобщенной. Вы можете присвоить R
значение по умолчанию для типа аргумента I
, чтобы когда TypeScript не может вывести R
из входных данных функции, он использовал I
. Таким образом, вызов, который пропускает transformData
, получит это значение по умолчанию, и тогда getData
будет ожидать I
как тип входных данных, как и задумано:
function transformer<I, R = I>({
getData,
transformData,
data
}: IFormData<I, R>): void;
Давайте протестируем это. Сначала, когда мы передаем transformData
:
transformer({
data: {
first_name: "",
last_name: "string",
age: 34
},
transformData: (data) => {
return {
fullname: data.first_name + " " + data.last_name,
age: data.age
}
},
getData(values) {
/* (параметр) values: {
fullname: string;
age: number;
} */
}
})
Выглядит хорошо. Здесь параметр data
функции transformData
имеет контекстный тип I
как {first_name: string, last_name: string, age: number}
от типа свойства data
. А параметр values
функции getData
контекстно типизирован как тип R
{fullname: string, age: number}
от возвращаемого значения функции transformData
.
Затем, вызывая без transformData
:
transformer({
data: {
first_name: "John",
last_name: "Makeev",
age: 34
},
getData(values) {
/* (параметр) values: {
first_name: string;
last_name: string;
age: number;
} */
}
})
Это также выглядит хорошо. Тип параметра values
функции getData
был контекстно типизирован как {first_name: string, last_name: string, age: number}
от значения по умолчанию для типа R
, которое совпадает с I
, который был выведен из типа свойства data
.
Ответ
Для решения проблемы манипуляции типами в вашем интерфейсе IFormData
, необходимо сделать его обобщённым (generic) как для входных данных I
, так и для возвращаемого типа функции transformData
.
Вот как может выглядеть ваш обобщённый интерфейс:
interface IFormData<I, R> {
data: I;
transformData?: (data: I) => R;
getData?: (values: R) => any;
}
Здесь используем I
для входящих данных, а R
для результирующего типа, возвращаемого из функции transformData
.
Также необходимо сделать вашу функцию transformer
обобщённой. Мы можем задать R
по умолчанию равным I
, чтобы в случае, если не будет передана функция transformData
, тип использовался по умолчанию. Вот так будет выглядеть функция transformer
:
function transformer<I, R = I>({
getData,
transformData,
data
}: IFormData<I, R>): void {
if (getData) {
if (transformData) {
getData(transformData(data)); // передаём результат трансформации
} else {
getData(data); // передаём исходные данные
}
}
}
Теперь давайте рассмотрим примеры использования этой функции:
- С передачей
transformData
:
transformer({
data: {
first_name: "",
last_name: "string",
age: 34
},
transformData: (data) => {
return {
fullname: data.first_name + " " + data.last_name,
age: data.age
}
},
getData(values) {
// Здесь 'values' будет иметь такой тип:
// { fullname: string; age: number }
}
});
В этом случае параметры data
и values
будут иметь корректные типы, так как TypeScript сможет выйти за пределы и точно определить, каков тип результат трансформации.
- Без
transformData
:
transformer({
data: {
first_name: "John",
last_name: "Makeev",
age: 34
},
getData(values) {
// Здесь 'values' будет иметь такой тип:
// { first_name: string; last_name: string; age: number }
}
});
В этом случае, когда мы не передаем transformData
, параметр values
будет типом I
, что также соответствует ожидаемому типу.
Таким образом, благодаря генерикам и параметрам по умолчанию, мы можем гарантировать, что типы передаются корректно в зависимости от того, передана ли функция transformData
или нет.
Вы можете протестировать приведённый выше код и увидеть, как TypeScript будет обеспечивать проверку типов для data
и values
в зависимости от предоставленных вами входных данных.