-
Notifications
You must be signed in to change notification settings - Fork 27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rewrite in Typescript #23
Comments
I might be approaching this wrong. Experimenting with the Typescript definitions above does not provide such a good typeahead experience as I would have hoped. For example, I was hoping that when I start typing Maybe the better solution would be Code Editor extensions instead? Or am I missing something? |
Ideally, I’d like a typeahead that is aware of the route I pass in as the first argument. So when I type |
I tried making the overloads mutually exclusive e.g. type PostFooRoute= "POST /foo";
function endpoint(route: GetFooRoute, options: GetFooOptions): GetFooRequestOptions;
function endpoint(route: PostFooRoute, options?: Options): RequestOptions;
function endpoint(route: Exclude<string, GetFooRoute | PostFooRoute>, options?: Options): RequestOptions; and wasn't able to quite get the behavior you desire. It looks like this is just not supported by TypeScript's intellisense service yet - but I think if you do it this way, as and when TypeScript eventually adds this feature, it will work in the future :) cc @DanielRosenwasser who can keep me honest here. |
I'd file a minimal "what we'd like to do" on the repo as a suggestion, and we'll figure out if we can make it happen. My understanding is it should be doable |
Setting it up as a single overload does what you want. The multi-overload case doesn;t work like you'd like, since even when the route is "known", if the argument type doesn't match, it can fall back to the unknown route overload. By not using any overloads at all and encoding all of the logic into the type of a single overload, we can guarantee that we switch the argument and return types to the desired types. |
Thanks for the trick @weswigham. Out of intellectual curiosity, why doesn't the overload case work even when the overloads are mutually exclusive? type PostFooRoute= "POST /foo";
function endpoint(route: GetFooRoute, options: GetFooOptions): GetFooRequestOptions;
function endpoint(route: PostFooRoute, options?: Options): RequestOptions;
function endpoint(route: Exclude<string, GetFooRoute | PostFooRoute>, options?: Options): RequestOptions; The 3rd overload that has an unknown route excludes the known "GET /foo" route, so
should not happen right? |
Until we merge negated types, the false branch of a conditional (like exclude) can't actually track mutual exclusivity on non-generic types. You'll find that |
Thanks @weswigham, that works! I wonder if something similar that would work to also support endpoint({
method: "GET",
url: "/get",
// autocomplete now suggests "foo" and "bar" for GetFooOptions
}) I tried this but am not getting anywhere interface ByMethodAndUrl {
"GET": {
"/foo": {
endpointOptions: { method: "GET", url: "/foo" } & GetFooOptions,
requestOptions: GetFooRequestOptions
}
}
}
function endpoint<M extends Method, U extends string>(
options: {
method: M,
url: U
} & U extends keyof ByMethodAndUrl[M] ? ByMethodAndUrl[M][U]["endpointOptions"] : EndpointOptions
): RequestOptions;
// Error: Type 'U' cannot be used to index type 'ByMethodAndUrl[M]'. Here is my current playground Thanks so much for all your help 👍 Update I tried this interface ByMethodAndUrl {
"GET": ByGetUrl,
"POST": ByPostUrl,
}
interface ByGetUrl {
"/foo": {
endpointOptions: GetFooOptions,
requestOptions: GetFooRequestOptions
}
}
interface ByPostUrl {
"/foo": {
endpointOptions: PostFooOptions,
requestOptions: PostFooRequestOptions
}
}
function endpoint<M extends keyof ByMethodAndUrl, U extends string>(
options: U extends keyof ByMethodAndUrl[M] ? ByMethodAndUrl[M][U]["endpointOptions"] : EndpointOptions
): RequestOptions; But getting
Although it looks to me just like
¯\_(ツ)_/¯ |
@weswigham one thing that your solution is lacking for When I start typing endpoint('GET /f it would be great if Typescript would suggest completing to Update That seems to do that trick function endpoint<R extends string>(
- route: R,
+ route: keyof ByRoute | R,
args: R extends keyof Routes ? Routes[R]["options"] : CustomRouteOptions
): R extends keyof Routes ? Routes[R]["requestOpts"] : RequestOptions; |
Surely I will be struck down for sharing types this arcane, but yes, it is probably possible, and with only a single source of truth for both varieties. |
okay wow that looks like it's not meant to be :) I also don’t think we can make both work side-by-side
Where both get validated based on known routes? I’ll leave the issue open, maybe it will be possible with a future version of Typescript. Thanks everybody! |
No, that works just fine. Your implementation signature is internal to the function implementation itself and you just have two (compatible) public overloads. |
Okay, wow, thanks! Now one last thing: For the first example, it correctly shows that the passed options are not compatible with the definition: the For the 2nd example though there is no such error. It should require the Any idea? 🙏 |
One error I now run into is that I cannot pass custom routes to endpoint({
method: 'GET',
url: '/unknown',
funky: 'fresh'
})
I’ve another question if you don’t mind. Is there a way in Typescript to support something like this const GetFooEndpoint = endpoint.defaults({
method: 'GET',
url: '/foo'
})
GetFooEndpoint({
// would require to set `"bar"` and `"baz"` options
// as `method: 'GET'` and `url: '/foo'` is set implicitly
}) I ask because that is basically how the entire https://github.com/octokit/rest.js API is defined. Right now I generate Typescript definitions for all endpoint methods such as
That’d be pretty rad :) |
The exact implementation depends on the type of |
@weswigham I’ve updated your playground example: link The current definitions show an error for endpoint({
method: "GET",
url: "/unknown"
});
// Type '"/unknown"' is not assignable to type '"/foo"'. But custom routes should be allowed. I tried to figure out how to fix that but failed :( |
Are their any tricks to debug a complex definitions such as function endpoint<T extends OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod][keyof OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod]]["options"]>(
options: T | (T extends OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod][keyof OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod]]["options"] ? OptionsByUrlAndMethod[T["url"]][T["method"]]["options"] : CustomRouteOptions)
): T extends OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod][keyof OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod]]["options"] ? OptionsByUrlAndMethod[T["url"]][T["method"]]["requestOpts"] : RequestOptions; When I test it against endpoint({
method: "GET",
url: "/unknown"
}); I’d love to understand which conditional types apply |
In this declaration: type KnownOptions = OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod][keyof OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod]]["options"];
function endpoint<T extends KnownOptions>(
options:
| T
| (T extends KnownOptions
? OptionsByUrlAndMethod[T["url"]][T["method"]]["options"]
: CustomRouteOptions)
): T extends KnownOptions
? OptionsByUrlAndMethod[T["url"]][T["method"]]["requestOpts"]
: RequestOptions; Why options:
| T
| (T extends KnownOptions
? OptionsByUrlAndMethod[T["url"]][T["method"]]["options"]
: CustomRouteOptions) why not just options: T extends KnownOptions
? OptionsByUrlAndMethod[T["url"]][T["method"]]["options"]
: CustomRouteOptions |
In the second, there's actually no location for us to infer
Break it out into smaller chunks of types and test them on individual input types. That's about the best advice I can give. |
Thank you so much for all your help. I ended up implementing the type definitions for One unfortunate bug (?) I run into is the autocomplete for enum strings that include a forward slash. See my playground. When you start typing "one" it will correctly list the three possible options As soon as I type "one/" the autocomplete disappears When I continue typing "one/two" all the options appear again, although only one of them should match When I then select the I see the same problem in the Typescript plugin for VS Code. This makes the definitions less usable. Could you point me to the right place where I can file a bug report for this particular issue? Thanks again for all your help 🙏 |
And do you know if there is any way I could add a description to the autocomplete options, instead of just showing the same text a 2nd time on the right side? |
In completions, the LHS is the "identifier" and the RHS is the "kind of type" (eg,
If you could open an issue on the TypeScript repo about that, it'd be great. I think it looks like we're picking up |
Done: microsoft/TypeScript#31304. Thanks! |
@weswigham would you mind me bugging you with a follow up question to your playground that you posted above: #23 (comment) This line is causing problems once there are route URLs with different methods: type WellKnownOptions = OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod][keyof OptionsByUrlAndMethod[keyof OptionsByUrlAndMethod]]["options"];
I've made a reduced test case here: playground You'll see that the |
I think I found a workaround by extending the definition of type OptionsByUrlAndMethod = UnionToIntersection<
{
[K in keyof Routes]: {
[TUrl in Routes[K]["request"]["url"]]: {
- [TMethod in Routes[K]["request"]["method"]]: {
- request: Routes[K]["request"],
- options: Routes[K]["options"] & {
- url: TUrl;
- method: TMethod;
- }
- }
+ [TMethod in Method]: TMethod extends Routes[K]["request"]["method"] ? {
+ request: Routes[K]["request"],
+ options: Routes[K]["options"] & {
+ url: TUrl;
+ method: TMethod;
+ }
+ } : never
}
}
}[keyof Routes]
>; That way the keys for all http methods are always set, not only the ones for the existing routes. |
Okay so I got it working in octokit/types.ts#50, but unfortunately I've now hit what looks like a TypeScript limit:
😭 |
I am trying to consume This is how I tried to consume this api from my service and that service from a component - Please provide some solution. |
Can you please open a new issue and not use old & closed issues. Also, you have posted a screenshot of your GitHub Personal Access Token, you should revoke it and generate a new one |
I was very cautious to write libraries in Typescript, I was always thinking that it might increase barriers to contributors. But we won’t get around having typescript definitions and the constant problems that occur due to JavaScript code being out-of-sync with the Typescript definitions I think it’s time to embrace Typescript.
If you have any concerns, now is the time to raise them. I’ll probably rewrite all
@octokit/*
libraries to Typescript.I started experimenting with Typescript definitions for
@ocotkit/endpoint
a while ago:My thinking is that we could use
@octokit/routes
to generate typescript definitions for every known route, I think that would provide a very sleek developer experience.The same definitions can be inherited by
@octokit/request
, so people might end up just using this library in order to minimize their bundle size, but still have nice typeahead experience.The text was updated successfully, but these errors were encountered: