Манипуляция типами с опциональной функцией

Вопросы и ответы

Нужна помощь с манипуляцией типов. У меня есть следующий интерфейс

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) // что-то сделать
        }
    }
}

Примеры:

  1. С 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
            }
        }
    })
    
  2. Без 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.

Ссылка на код в Playground

Ответ

Для решения проблемы манипуляции типами в вашем интерфейсе 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); // передаём исходные данные
        }
    }
}

Теперь давайте рассмотрим примеры использования этой функции:

  1. С передачей 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 сможет выйти за пределы и точно определить, каков тип результат трансформации.

  1. Без 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 в зависимости от предоставленных вами входных данных.

Оцените материал
Добавить комментарий

Капча загружается...