-
Notifications
You must be signed in to change notification settings - Fork 254
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
Add type declarations and ziggy.d.ts
generation
#664
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@lmeysel I've been slowly reworking this into a single index.d.ts
that we can ship as-is and still support autocompletion, and what I've got here is obviously unfinished but it seems to be working!
I still don't quite understand how some of the details work... I've left specific questions on lines that are confusing me, and if you have time and don't mind I would really appreciate any explanations you can add. No pressure of course!
/** | ||
* A route name, or any string. | ||
*/ | ||
type RouteName = KnownRouteName | (string & {}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I answered my own earlier question on the original PR here by playing around with it—string
makes autocompletion stop working, I guess because it's too wide a type, but (string & {})
still supports autocompletion while also allowing any old string. Does this functionality have a name? Something about it being 'exhaustive'?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I also thought very long about it but did not find why this works. I just found that somewhere on SO :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hahaa okay, thanks! Any thoughts about my other two questions below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, there some beautiful changes at home having my attention right now, so I am a bit slower in answering currently :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No worries at all, I really appreciate these answers it's super helpful ❤️
src/js/index.d.ts
Outdated
/** | ||
* An object of parameters for a specific named route registered with Ziggy. | ||
*/ | ||
type KnownRouteParamsObject<I extends readonly ParameterInfo[]> = { [T in I[number] as T['name']]?: ValueOrBoundValue<T> | string } & HasQueryParam; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is readonly
doing in I extends readonly ParameterInfo[]>
?
How exactly does T in I[number] as T['name']
work? I can kind of wrap my head around what it ends up accomplishing, but the syntax isn't clicking for me. Does I[number]
enable a second level of iteration through I
? Why not just T in I
?
Why | string
? In my testing everything seems to work the same without it, and ValueOrBoundValue<T>
already includes string
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is readonly doing in I extends readonly ParameterInfo[]>?
Taking ParameterInfo[]
as readonly
makes TS consider it as const
array. I.e. there is a difference between
const parameterInfo: ParameterInfo[] = [{ ...paramInfo1, name: 'pi1' }, { ...paramInfo2, name: 'pi2' }]
const roParameterInfo: ParameterInfo[] = [{ ...paramInfo1, name: 'pi1' }, { ...paramInfo2, name: 'pi2' }] as const
The expressions allow typescript to resolves name the exact string literals, whereas the first one resolves to string. So:
type Names = (typeof parameterInfo)[number]['name'] // String but
type RoNames = (typeof roParamterInfo)[number]['name'] // 'pi1' | 'pi2'
Disclaimer: I am not sure if these snippets exactly reproduce in TS, but this is the basic principle behind that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How exactly does T in I[number] as T['name'] work? I can kind of wrap my head around what it ends up accomplishing, but the syntax isn't clicking for me. Does I[number] enable a second level of iteration through I? Why not just T in I?
Given that
type I = readonly Array<{ name: string }>
Then we can map it to an object where each key is the name of each array element. E.g refine the expression above to a more concrete:
const arr = [{ name: 'foo' }, { name: 'bar' }] as const;
type Q = typeof arr;
Now we create an object from that array where the key is the name property of all elements:
type ObjType = { [K in keyof Q[number] as K['name']]: SomeType }
// ^ ^^^^^^^^^ ^^^^^^^^^
// | | The actual key name, e.g. 'foo' | 'bar'
// The "iteratee" is not the array itself (since `keyof []` is push, pop, splice, ....) but the elements
// The "iteration" variable which is basically the type of each element in the array
Thus, the type of ObjType
from the concrete Q
is mapped to
type ObjTypeOfQ = {
foo: SomeType
bar: SomeType
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why | string? In my testing everything seems to work the same without it, and ValueOrBoundValue already includes string.
Probably a mistake. I did a lot of experiments, created new types, dropped them, moved some parts of the unions around. Probably just a relict of that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very cool, that makes sense and is super helpful. Thanks a lot!
src/js/index.d.ts
Outdated
/** | ||
* An array of parameters for a specific named route registered with Ziggy. | ||
*/ | ||
type KnownRouteParamsArray<I extends readonly ParameterInfo[]> = { [K in keyof I]: ValueOrBoundValue<I[K]> | string }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this type become an array? I can see that it works, but I don't understand how—the type definition { [K in keyof I]: ValueOrBoundValue<I[K]> | string }
looks like an object type, I would have expected it to produce something like { uuid: ParameterValue }
. Does it have to do with the keyof
in K in keyof I
?
Same question here about | string
too, seems to work fine without it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this type become an array? I can see that it works, but I don't understand how—the type definition { [K in keyof I]: ValueOrBoundValue<I[K]> | string } looks like an object type, I would have expected it to produce something like { uuid: ParameterValue }. Does it have to do with the keyof in K in keyof I?
That was difficult for me as well: Basically, since I
is an array, keyof I
is a number. On a readonly
array, this expression allows to remap the type in an const array. I did not find something about this behavior in the TS documentation, but only on SO and TS seems to be aware of this particular expression and then makes the result an array type again. (What looks to you as an object like { 0: MyType, 1: MyType }
is considered as array [MyType, MyType]
Caution: Be aware, this expression is very sensible. E.g. the result becomes an object if you directly make a union with this expression like { [K in keyof I]: SomType } | string
.
Additionally, If I remember correctly, this is tied to one of the more recent versions of TypeScript.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here about | string too, seems to work fine without it?
Same answer as above ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wild. Thanks so much!
…`Router` type, fix `route()` exports
…ow arbitrary extra object properties in params
…rs (not as binding values)
No idea what's going on with the line endings... the |
…nt Microbundle from overwriting types
… where we only want arrays
… more metadata there than just names)
This PR does two things: it adds types for Ziggy itself, and it adds the ability to generate type declarations specific to a user's list of routes. Credit for most of this goes to @lmeysel for the original implementation in #655.
Types
This PR adds an
index.d.ts
file containing type definitions for Ziggy itself. These types will be maintained manually since Ziggy is written in JavaScript. Out of the box they should be able to replace@types/ziggy-js
and provide much smoother autocompletion and type hinting for end users.They're currently tested manually using
tests/js/types.ts
, which contains sample Ziggy code that should or shouldn't produce type errors.User route types
This PR also adds
--types
and--types-only
options to theziggy:generate
command to generate aziggy.d.ts
file containing additional types specific to the app using Ziggy. This file extends Ziggy'sRouteList
interface to list the names and some basic parameter metadata about all the routes in the app, which allows IDE autocompletion of routes and parameter names while using theroute()
function.