Типовая система 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
Выглядит хорошо для меня. Надеюсь, это поможет;удачи!
Ссылка на код