Простое, безопасное с точки зрения типов использование API в Typescript - PullRequest
0 голосов
/ 05 июля 2019

Я изо всех сил пытался назвать этот вопрос - можно изменить его.

Я довольно новичок в машинописи и пытаюсь использовать API универсальным, безопасным и расширяемым способом.

Вдохновившись RESTyped , я определил универсальный интерфейс "определения API":

interface ApiBase {
    [route: string]: ApiRoute   
}

interface ApiRoute {
    query: { [key: string]: string }
    body: any
    response: any
}

interface ApiSpec {
    [route: string]: {
        [method: string]: ApiRoute  
    }
}

, и его можно использовать для определения типов для нескольких конечных точек APIвот так:

interface MyApi extends ApiSpec {
    "/login": {
        "POST": {
            body: {
                username: string,
                password: string
            },
            response: {
                token: string
            }   
        }   
    },
    "/user": {
        "GET": {
            query: {
                "username": string
            },
            response: {
                "email": string,
                "name": string
            }
        }
    }
}

Я подозреваю, что универсальный класс может использовать эти типы и предоставлять для них безопасные для типов методы.Что-то вроде:

const api = ApiService<MyApi>();
api.post("/login", {
    // This body is typesafe - won't compile if it doesn't match the spec
    username: "johnny99",
    password: "hunter2"
});

Где метод post() не скомпилируется, если объект не соответствует body, определенному в интерфейсе MyApi.

К сожалению, яЯ довольно потерян для того, чтобы идти отсюда.Примерно так:

class ApiService<T> {
    post(route: string, body: T[route].body): T[route].response {
        // todo
    }
}

Что явно не компилируется.Как я могу получить доступ к подтипу в интерфейсе MyApi?T[route].body определенно не так.Как мне это сделать?

Ура

РЕДАКТИРОВАТЬ ------------------------------------------

Я немного почитал, и мне кажется, что я чего-то добиваюсь!

Это работает на площадке для машинописи:

class ApiService<API extends ApiSpec> {
    async post<Path extends Extract<keyof API, string>>(
        route: Path,
        data: API[Path]["POST"]["body"]
    ): Promise<API[Path]["response"]> {
        const resp = await fetch(route, {
            method: "POST",
            body: JSON.stringify(data),
        });
        return await resp.json();
    }
}

И отлично работает при вызове маршрута, который существует:

const api = new ApiService<MyApi>();

// Will give an error if the wrong "body" is passed in!
api.post("/login", {
    username: "johnny99",
    password: "rte"
});

, но он также работает при вызове маршрута, который не существует, а это не то, что я хочуслучиться.

// Should error, but doesn't!
api.post("/bad", {
    whatever: ""
});

Я также немного обеспокоен моей реализацией post() - что происходит, когда объект, заданный resp.json(), отличается от того, что определено в определении типа?Приведет ли это к ошибке во время выполнения - я должен всегда вызывать ее в try/catch страже, или я могу каким-то образом заставить Promise провалиться вместо этого?

1 Ответ

1 голос
/ 05 июля 2019

Прежде чем я получил ответ, я попытался воспроизвести вашу ситуацию на игровой площадке и заметил, что мне нужно изменить тип ApiRoute на

interface ApiRoute {
  query?: { [key: string]: string }; // optional
  body?: any; // optional
  response: any;
}

чтобы избежать ошибок. Если это не было для вас ошибкой, это потому, что вы не используете --strictNullChecks, что вам действительно следует. Я предполагаю, что теперь мы проводим строгую проверку на ноль.


Я думаю, что ваша проблема здесь в том, что ваш ApiSpec интерфейс говорит, что у него есть ApiRoute свойства для каждого возможного ключа и каждого возможного подключа :

declare const myApi: MyApi;
myApi.mumbo.jumbo; // ApiRoute
myApi.bad.POST.body; // any

Этот код не является ошибкой. Итак, когда вы звоните

api.post("/bad", {
  whatever: ""
});

вы просто ищете body свойство некоторого myApi.bad.POST, что не является ошибкой.


Так как мы можем это исправить? Я думаю, что было бы более разумно выразить тип ApiSpec как универсальное ограничение на возможных MyApi -подобных типах вместо конкретного типа с парой вложенных подписей индекса:

type EnsureAPIMeetsSpec<A extends object> = {
  [P in keyof A]: { [M in keyof A[P]]: ApiRoute }
};

Это сопоставленный тип , который превращает A как {foo: {bar: number, baz: string}} в {foo: {bar: ApiRoute, baz: ApiRoute}}. Так что если у вас есть A extends EnsureAPIMeetsSpec<A>, то вы знаете, что A соответствует вашим намеченным спецификациям (более или менее ... Я думаю, вы могли бы подумать, чтобы убедиться, что каждое свойство A само по себе имеет тип object).

И вам не нужно говорить MyApi extends ApiSpec. Вы можете просто оставить это как

interface MyApi { /* ... */ }

и, если оно плохое, оно не будет принято ApiService. Или, если вы хотите знать сразу, вы можете сделать это так:

interface MyApi extends EnsureAPIMeetsSpec<MyApi> { /* ... */ }

Теперь определим ApiService. Прежде чем мы туда доберемся, давайте создадим несколько помощников типов, которые мы будем использовать в ближайшее время. Во-первых, PathsForMethod<A, M> принимает тип API A и имя метода M и возвращает список строковых путей, поддерживающих этот метод:

type PathsForMethod<A extends EnsureAPIMeetsSpec<A>, M extends keyof any> = {
  [P in keyof A]: M extends keyof A[P] ? (P extends string ? P : never) : never
}[keyof A];

А потом Lookup<T, K>:

type Lookup<T, K> = K extends keyof T ? T[K] : never;

в основном T[K] за исключением того, что компилятор не может проверить, что K является ключом T, он возвращает never вместо выдачи ошибки компилятора. Это будет полезно, потому что компилятор не достаточно умен, чтобы понять, что A[PathsForMethod<A, "POST">] имеет ключ "POST", хотя именно так был определен PathsForMethod. Это морщина, которую мы должны преодолеть.

Хорошо, вот класс:

class ApiService<A extends EnsureAPIMeetsSpec<A>> {
  async post<P extends PathsForMethod<A, "POST">>(
    route: P,
    data: Lookup<A[P], "POST">["body"]
  ): Promise<Lookup<A[P], "POST">["response"]> {
    const resp = await fetch(route, {
      method: "POST",
      body: JSON.stringify(data)
    });
    return await resp.json();
  }
}

Проходя через это ... мы ограничиваем A до EnsureAPIMeetsSpec<A>. Затем мы ограничиваем параметр route только теми путями в PathsForMethod<A, "POST">. Это автоматически исключит "/bad" route, которые вы пробовали в своем коде. Наконец, мы не можем просто сделать A[P]["POST"] без ошибки компилятора, поэтому вместо этого мы делаем Lookup<A[P], "POST">, и все работает нормально:

const api = new ApiService<MyApi>(); // accepted

const loginResponse = api.post("/login", {
  username: "johnny99",
  password: "rte"
});
// const loginResponse: Promise<{ token: string; }>

api.post("/bad", { // error!
  whatever: ""
}); // "/bad" doesn't work

Вот так я бы начал. После этого вы можете сузить определение ApiSpec и ApiRoute. Например, возможно, вы хотите два типа ApiRoute, некоторые из которых требуют body, а другие запрещают его. И вы, вероятно, можете представить свои методы http как некоторое объединение строковых литералов, таких как "POST" | "GET" | "PUT" | "DELETE" | ... и сужение ApiSpec, так что методы "POST" требуют body, в то время как методы "GET" запрещают это, и т. Д. Это могло бы сделать это проще чтобы компилятор гарантировал, что вы вызываете post() только на правильных путях и что body таких сообщений будет требоваться и определяться вместо, возможно, неопределенного.

В любом случае, надеюсь, это поможет; удачи!

Ссылка на код

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...