Как динамически определить возвращаемый тип в зависимости от аргумента функции
Есть клиентское API, набор функций, выполняющих запрос на сервер. К сожалению, серверное API написано не идеально, и ответ приходит в разных форматах. На стороне клиента, эти форматы я привожу к стандартному типу
Успех
interface SuccessResponse<T> {
data: T;
status: 'ok';
}
Ошибка
export interface ErrorResponse {
error: Error; // Тип Error
status: 'error';
}
Сама функция в упрощенном виде выглядит вот так.
return fetch(url, method, data)
.then((res): SuccessResponse<User> => res)
.catch((error): ErrorResponse => error);
Тип в данном примере зависит от аргументов url
и method
Обратите внимание, что в дженерик я подставляю тип User, это "жесткая" запись, для определенного URLа /use/auth и метода GET
Если я поменяю URL на book/add POST, то ответ от сервера уже будет другой. Соответственно, мне надо как-то переключить тип на SuccessResponse<Book>. Наверное, по-хорошему, надо делать как-то так: SuccessResponse<url, method>, но я не представляю, как переключить тип data в интерфейсе SuccessResponse. Подскажите, можно ли как-то динамически поменять тип?
Хотелось бы оставить названия типов User и Book, как есть, для лучшей читаемости, и просто связать их в каком-нибудь объекте с нужным URLом и уже с помощью него делать выборку. Пока мало опыта с TS не представляю как это правильно делается.
Или другой вариант, тоже мне кажется хорошим решением: в том месте, где я вызываю эту функцию передавать тип User. Но тоже непонятно, можно ли прокидывать типы от функции к функции вглубь.
Дополнение
Понял что что пример выше не годится для понимания вопроса. Распишу более подробно что делает функция и в каком виде возвращается объект.
1. базовая функция для отправки запроса на сервер
вырезал все что не относится к вопросу.
const fetch = (url, method, data) => {
const fetchConfig = buildFetchConfig({ method, data }); // Дополнительная функция, где определяются заголовки, префиксы, методы и прочее..
return $fetch(url, fetchConfig)
.then((res) => res)
.catch((error) => error);
};
Она может вернуть объект двух типов успех/ошибка (см. начало вопроса).
3. Класс, реализует две метода
class Request {
static get(url, params) {
return fetch(url, 'get', params);
}
static post(url, body) {
return fetch(url, 'post', body);
}
}
4 ну и собственно набор роутов, делающих реквест
class User {
static auth() {
return Request.get('user/auth');
}
static login(body) {
return Request.post('user/login', body);
}
}
class Book {
static add(body) {
return Request.post('book/add', body);
}
}
Реализация хромает еще причешу.
Все функции и методы в данном пример общие, то есть могут вызываться из любого участка кода с разными аргументами, которые и должны определять возвращаемый тип объекта.
В моем примере единственное место, где я уже точно знаю, какой тип мне должен вернуться - это методы User.auth() User.login.. и т.д.
А теперь как обращаюсь к User.auth() и какой тип получаю
const res = await UserApi.auth(); // const res: any
// соответственно, код ниже уже не дает никакой подсветки
if (res.status === 'ok') {
this.user = res.data.somevar; // не показывает ошибку.
}
Что ожидаю получить и что пробовал
Пробовал добавить тип в метод auth() таким образом:
type ApiResponse<T> = SuccessResponse<T> | ErrorResponse
static auth(): Promise<ApiResponse<User>> {
И это работает
const res = await UserApi.auth(); // const res: ApiResponse<User>
if (res.status === 'ok') {
this.user = res.data.somevar; // TS2339: Property 'somevar' does not exist on type 'User'
}
Но не определяет, какой тип возвращают остальные функции. Можно ли как-то динамически определить тип для них, когда аргументы определяются только в методах классов User и Book?
То есть, по-хорошему, еще на этом (самом верхнем уровне) я должен определять тип, и прокидывать его в глубь, к самой первой функции. Предполагаю что это в таком кейсе в тайпскрипте это делается именно так по цепочке. Возможно есть какое-то другое решениt. Опыт с TS только получаю.
Ответы (2 шт):
Документация. Немного геморный вариант, но рабочий:
interface IUser {
name: string,
age: number
}
interface IBook {
name: string,
author: string,
pages: number
}
enum Paths {
AUTH = '/use/auth',
BOOK = 'book/add',
}
enum Methods {
GET = 'GET',
POST = 'POST',
}
type objectType<T, K> =
T extends Paths.AUTH ? K extends Methods.GET ? IUser : null : null &
T extends Paths.BOOK ? K extends Methods.POST ? IBook : null : null;
function getObject<T, K>(path: Paths, method: Methods): objectType<T, K> {
return {} as any;
}
getObject<Paths.AUTH, Methods.GET>(Paths.AUTH, Methods.GET) // IUser
getObject<Paths.BOOK, Methods.POST>(Paths.BOOK, Methods.POST) // IBook
Не совсем понятно при каких условиях какие типы должны получаться, но вот простой пример как такое можно сделать:
// Определяем возвращаемые типы для методов, все строки можно вынести в енумы
type MethodReturnTypes<T> = {
"get": T;
"post": null;
};
// Получаем список всех доступных методов для удобства
type Methods = keyof MethodReturnTypes<never>;
// Определяем доступные пути
type UserPaths = `user/${"login" | "auth"}`;
type BookPaths = `book/${"add"}`;
// Задаём типы методов для каждой группы путей
type PathMap =
& { [path in UserPaths]: MethodReturnTypes<User>; }
& { [path in BookPaths]: MethodReturnTypes<Book>; };
// Получаем список всех доступных путей для удобства
type Paths = keyof PathMap;
type Response<P extends Paths, M extends Methods> = PathMap[P][M];
type t1 = Response<"book/add", "post">; // null
type t2 = Response<"user/auth", "get">; // User
Функцию для работы с этим можно определить так:
function fetch<P extends Paths, M extends Methods>(url: P, method: M): Response<P, M>;
const t1 = fetch("book/add", "post"); // null
const t2 = fetch("user/auth", "get"); // User