Skip to content
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

Merged
merged 50 commits into from
Oct 6, 2023
Merged

Conversation

bakerkretzmar
Copy link
Collaborator

@bakerkretzmar bakerkretzmar commented Sep 1, 2023

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 the ziggy:generate command to generate a ziggy.d.ts file containing additional types specific to the app using Ziggy. This file extends Ziggy's RouteList 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 the route() function.

@bakerkretzmar bakerkretzmar self-assigned this Sep 1, 2023
Copy link
Collaborator Author

@bakerkretzmar bakerkretzmar left a 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 & {});
Copy link
Collaborator Author

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'?

Copy link

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 :)

Copy link
Collaborator Author

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?

Copy link

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 :)

Copy link
Collaborator Author

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 ❤️

/**
* 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;
Copy link
Collaborator Author

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.

Copy link

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.

Copy link

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
} 

Copy link

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.

Copy link
Collaborator Author

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!

/**
* 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 };
Copy link
Collaborator Author

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?

Copy link

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.

Copy link

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 ;)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wild. Thanks so much!

@bakerkretzmar
Copy link
Collaborator Author

No idea what's going on with the line endings... the .js files work fine.

@bakerkretzmar bakerkretzmar marked this pull request as ready for review October 3, 2023 21:49
@bakerkretzmar bakerkretzmar merged commit 141f00d into main Oct 6, 2023
@bakerkretzmar bakerkretzmar deleted the dts-generation branch October 6, 2023 13:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants