Typescript выводит тип объединения вместо указанного типа - PullRequest
1 голос
/ 04 ноября 2019

Я хочу встроить «метаданные» в тип для использования при создании REST-клиента с безопасным типом. Идея состоит в том, чтобы использовать метаданные типа в ссылке, чтобы вывести правильную схему конечной точки для использования в вызове API. Например,

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

type User = {
  self: Link<"users">;
};

const user: User = { self: "https://..." };

http(user.self, "GET", { userId: 1 });

Я смог заставить это работать с условными типами грубой силы.

Например,

type Routes = "users" | "posts";
type Verbs<R> = R extends "users" ? "GET" : never;
type Query<R, V> = R extends "users"
  ? V extends "GET"
    ? { queryId: string }
    : never
  : never;

Но это приводит к модели нормализованного типа, которую было бы сложно ввести вручную. Вместо этого я хочу использовать ненормализованный тип Eg

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

Используя такие типы:

type Query<
  S,
  RN extends keyof S,
  VN extends keyof S[RN]
> = OpQuery<S[RN][VN]>;

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

type Schema = {
  users: {
    GET: {
      query: { userId: string };
    };
  };
  posts: {
    POST: {};
  };
};

type Link<R extends keyof Schema> = string;

type LinkRouteName<L> = L extends Link<infer R> ? R : never;

type name = LinkRouteName<Link<"users">>;

ожидаемый: имя === "пользователи"

фактический: имя === "пользователи" |"сообщения"

1 Ответ

1 голос
/ 04 ноября 2019

Типовая система TypeScript структурная , а не номинальная, что означает, что это форма типа, который определяет его идентичность, а не имя типа,Псевдоним типа, например

type Link<R extends keyof Schema> = string

, не определяет тип, который каким-либо образом зависит от R. И Link<"users">, и Link<"posts"> оцениваются как string;это просто разные имена для одного и того же типа, и, следовательно, не имеют значения для системы типов. Теоретически эти два типа неотличимы друг от друга ... бывают случаи, когда компилятор может различать два типа одинаковой формы, такие как эти разные имена, но вы никогда не должны полагаться на это.

В любом случае, информация из типа R выбрасывается, и следующее не может вернуть ее:

type LinkRouteName<L> = L extends Link<infer R> ? R : never;

И LinkRouteName<Link<"users">>, и LinkRouteName<Link<"posts">> оцениваются какLinkRoutName<string>, из которого не может быть определено ничего более определенного за общим ограничением на R в определении Link<R>: то есть keyof Schema, он же "users" | "posts". В FAQ по TypeScript есть аналогичный пример , где вывод типа не может вернуть информацию об исключенном типе.


Так что, если вы хотите, чтобы два типа обрабатывались по-разному, они должны иметь разныеструктур. Если бы Link<R> был типом объекта, я бы предложил добавить к этому объекту свойство, скажем, name, со значением типа R.

Но вы просто используете примитивный тип string. Получить примитивный тип структурно отличаться невозможно во время выполнения (вы не можете добавить к нему такие свойства, как (var a = ""; a.prop = 0;)). Вы могли бы использовать String тип оболочки и добавлять свойства к нему, если хотите.

Еще один способ - ввести в заблуждение компилятор для обработки примитива. string напечатало значение так, как если бы оно структурно отличалось от string, используя то, что называется " фирменные примитивы ". Вы пересекаете примитивный тип с фантомным свойством «brand», которое используется для различения типа. Мое предложение здесь будет:

type Link<S extends keyof Schema> = string & { __schema?: S };

Свойство фантом не является обязательным, поэтому вам будет разрешено писать

const userLink: Link<"users"> = "anyStringYouWant";

без утверждения типа , но выдолжны убедиться, что вручную аннотируют тип. Следующее не будет работать:

const userLink = "anyStringYouWant";

Это будет просто string, а не Link<"users">.


Как только вы это сделаете, остальное должновстать на место. Возможное объявление для функции http() может быть следующим:

declare function http<
  S extends keyof Schema,
  V extends keyof Schema[S],
  >(
    url: Link<S>,
    verb: V,
    ...[query]: Schema[S][V] extends { query: infer Q } ? [Q] : []
  ): void;

, использующее типы остальных кортежей для представления того, что http() может принимать или не принимать третий параметр в зависимости от того,не соответствующая запись Schema имеет соответствующее свойство query.

Давайте проверим, что это работает:

type User = { self: Link<"users"> };
const user: User = { self: "https://..." };
http(user.self, "GET", { userId: "1" }); // okay
http(user.self, "GET", {}); // error!  userId missing
http(user.self, "GET"); // error! expected 3 arguments

type Post = { self: Link<"posts"> }
const post: Post = { self: "https://..." }
http(post.self, "POST"); // okay 
http(post.self, "POST", { userId: "1" }); // error! expected 2 arguments

Выглядит хорошо для меня. Надеюсь, это поможет;удачи!

Ссылка на код

...