-
-
Notifications
You must be signed in to change notification settings - Fork 648
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
Update to Recursive Type reference #1764
Conversation
Thanks for your PR! I understand that there is some kind of type recursion that need to be solved and I'm glad you found a way to fix this. Just thinking whether this could make it impossible to change I'll be looking more into it later today or monday at latest. |
Looking at it again, I believe the creating hook should take a TInsertType while updating and reading hook take a T, because the TInsertType is the type passed to Table.add() and T is the type actually stored in the database and retrieved from it. I'm also curious to understand how these changes avoid the recursiveness. Might it be something that could rather be solved in UpdateSpec? |
I believe you are correct the main recursion (Bug) happened in the "UpdateSpec" When using a Complex Class structure the TInsertType keeps a nice abstraction layer between the actual class and the stored data. The important thing to understand is the DataClass should theoretically always implement the TInsertType. So even if you pass the Class to the insert or update it is interface compatible and there is no need to analyze the Class Type for a partial update etc.. Even if it could be solved with a better UpdateSpec it would be extremely hard to account for every complex class structure other than the most basic DataObjects. I added the TInsertType to the reading hook because when deserializing to a class object the data coming from the IndexedDb is the raw TInsertType, then in the hook receives that TInsertType and returns the Deserialized Real DataClass type T. I was basing this off the mapToClass function. I created a special version of this class to do a more sophisticated deserialization but it basically followed the same pattern as the original. I didn't apply the change to the Creating hook as I was not using it, but I believe the application would be the same with the TInsertType as that is the closest type to what is actually being stored. I wasn't 100% which is why I didn't touch it. One could make a case that all the hooks should be the TInsertType as it's such a low level and what matches what is stored in the IndexDb. I guess when using a custom deserializer or there is a chain of readers this may be problematic since the structure should always be the same in the hook chain. The only way to solve it if I'm understanding it correctly is have a single instance hook api for deserialization. That way only "One" process (method) is responsible for going from the serialized structure to the Class instance per table, Then from there on the class instance can be passed to all the extensions hooking Dexie ie. reading would pass around the DataClass T. kinda like a singleton hook "deserialize". the native mapToClass would register it and if someone needed their own serialization they could inject their own mapToClass implementation. or take over the "deserialize". rendering the native mapToClass disabled for a particular table. Seems like mapToClass suffers from the same issue of multiple readers , it would have to be first in the chain also the first reader gets the TInsertType converts it and the rest of the listeners would get a different Class Type , now I'm understanding the tension with the Typing the reading hook. One could take a position that all extensibility via hooks and dbcore is done in the TInsertType format allowing Dexie and any plugins/third parties to rely solely on the serialized JSON structure which matches ultimately what is being stored in the DB "POJO", and anything being returned via queries to the end user of the library either gets the T Type which would be the POJO or a deserialized Class if using mapToClass or a custom deserializer. This could help prevent against a leaky abstraction. Happy to brainstorm some more if needed... |
Update from master
Any update to this? There still is a Recursive Typing Error in the master branch. |
I understand about the read hook being the object type. I believe that change was mainly base on the way mapToClass was implemented in the core. The use of this method the mapping process would have to assume the obj: Object is the TInsertType to then return T. there is definitely is a discrepancy if there are down stream readers that want to only interact with the TInsertType. Maybe there is a more concrete way to do class mapping that only allows the ONE transformation (class mapping) operation at the end of the reading pipeline -> to a new fetching? pipeline that then passes the T type (class) along. That way a simple JSON is the result of the reading sent to a replaceable (mapToClass) which then starts the fetching hook for consumers only interested in the final T type (which most likely will be the project owner) // Reading is for the underlying serialized indexDb TInsertType // Fetching is for the mapped class type T (if the user is doing class mapping) |
Also, I believe the Type Recursion is mainly happening with Table Typeupdate( The UpdateSpec should be --- UpdateSpec Since Updating is a partial operation direct to the database. It makes more sense for a user to assume a Partial version TInsertType similar to the add() or put() When giving the Generic UpdateSpec a complex class type full of getters , references to other classes etc. It is failing to traverse all but the most simple interface types. |
My thoughts is that the type passed to reading hooks should be a Was the last commit on dexie export import intentional? |
I reverted my changes with the reading hook , everything is fine there at least it still works being cast as any. It's the UpdateSpec in the table.d.ts type that needs to be UpdateSpec. which will fix the Recursion Issue. I can undo my Branch and just submit the PR for this change by itself. The import / Export changes were submitted under a different branch and PR. I may have accidentally committed them to the TypesFix Branch. |
I see. I really apprechiate the work you've done on this and I think dexie4 might have 2 issues: 1) the recursion issue (that would be fixed in the UpdateSpec), then we have the fact that reading hooks typings lie if T is a class with methods - which is perfectly doable with mapToClass etc. I think the PR as it is is pretty close to solving them both, so I could also take over and introduce a |
@nlaurie Would you mind checking #1897 would solve your case? It's based on your branch but I introduced the Pojo type instead to get more proper typings for the hooks. Basically:
I haven't tried whether this solves the type recursion issue but maybe you have a chance to see if it solves it the same way as your original PR? |
Line 48 in table.d.ts. Fixes the Recursive Typing issue. As far as the pojo. short answer Long Answer and someone with their own db implementation who is interested in these hooks will want the fully typed T during the execution of the hooks. It really depends on who are the consumers of the hooks. I believe ultimately it's the developer writing a application and wants to get events on the DB but not go through the hassle of writing a DBcore Plugin. In that case they care only about T and TInsertType. Their Class Object and Serialized Representation. I would only add one thing and its for people that want to implement their own serialization and/or class mapping. The built in one was too generic for our project so we just followed the same pattern and copied the internal mapToClass. That lead to using the reading hook in a funny way. since the reading hook passes in one thing and then replaces it with another and this can happen multiple time if there are many listeners (plus order may matter) it works decent for simple objects but complex stateful classes the user may not want anyone replacing them Technically there should only be once place in code that goes form the DB Structure TInsertType to the final exposed T given to the consumer of dixie. (noted in many simple cases those types will be the same and not mapped) , but for the more advanced app you have the option of working with Class Objects or enhanced Objects. maybe allow the user to replace the mapToClass function when defining the Table. ie.
I could take a stab at the implementation. that way there is an internal single place inside Dexie that gets called if/when mapping is necessary. the result of the function is always T. then whenever Dexie needs the T it has a non leaky abstraction to go from TInsertType to T , and users can implement there own serialization. |
This reverts commit 1bfb4a6.
Leaving the reading hook as you suggested. Tried to repro issues with recursive types and add it in test-typings but I couldn't find a way (don't know how to repro). We still have a similar signature for Collection.modify() in collection.d.ts where it takes an |
I can try and create a simple environment to repro it. Here is the main compiler error ( our code base has over half million lines of code so may be hard to pin down the exact source) ultimately by avoiding complex type manipulation on the type T you are abstracted from any complex deserialized classes error TS2615: Type of property 'tables' circularly references itself in mapped type '{ [P in keyof Dexie]: P extends string ? Dexie[P] extends (infer K)[] ? K extends object ? P | |
Hmmm... how does the Dexie type come in as a T there? Are you using |
Here you go, easy repro |
If the issue would be due to providing the Dexie instance from a getter of an entity (like export type KeyPaths<T> = {
[P in keyof T]:
P extends string
? T[P] extends Dexie ? never : // AVOID RECURSIVENESS
? T[P] extends Array<infer K>
? K extends object
? P | `${P}.${number}` | `${P}.${number}.${KeyPaths<K>}`
: P | `${P}.${number}`
: T[P] extends (...args: any[]) => any // Method
? never
: T[P] extends object
? P | `${P}.${KeyPaths<T[P]>}`
: P
: never;
}[keyof T]; |
Funny thing , wasn't even aware of EntityTable. but it doesn't change anything in the repo. interesting enough when doing updates I don't even consider wanting to pass the T type to Dexie only the TInsertType , which is why |
I tried the repro and it somewhat makes it clearer that you need to express different type in update/modify than what the db returns. Tried with typescript@latest also and it doesn't complain though, but doesn't either provide any decent code completion for the update method - seems it gives up on the type rather than complaining about recursion. The TInsertType happens to be the only type to express. It's purpose was for add and put methods - mainly to get a solution for not having to have Having TInsertType in Table.update() and Collection.modify() would make it possible to work around the problems that arise from the mapped type UpdateSpec if T has recursive properties. Seems though that Typescript has "solved" this in the latest version. A perfect solution could have been to allow a 4th argument I'm not keen on introduction a 4th argument though and might go for your suggestion and use TInsertType in both Table.update() and Collection.modify() so that we can move on from here without adding too much of complexity. |
Thanks for your efforts and for the repro 💯 |
Im starting to understand your requirements for TInsertType now, as ours are slightly different. Maybe simply renaming TInsertType to TSerialized will offer more clarity to the developers. Our app separates the actions into three different interfaces Create, Update, PartialUpdate & Model I would have to further understand your use with autogen PKs we handle that with the "Model" having everything and the "Create" "Update" and "PartialUpdate" omit them. its a lot of work to maintain but offers complete type safety. I do agree to keep it simple maybe you just represent the full model and type the auto generated as optional. not perfect but allows you to put without the key and potentially rekey the object with an update. if necessary. |
This is a typing change that opens up for library users to solve issues with the complex mapped type `UpdateSpec<T>` expected in Table.update() and Collection.modify(). In case T is a complex class with recursive references, the UpdateSpec might fail due to exhausting the typescript compiler. Now, this can be avoided by providing a simpler version of T to TInsertType argument of `Table<T, TKey, TInsertType>`. The original intention of TInsertType is to be the POJO representation of an object that can be passed to Table.add(), Table.put() and their corresponding bulk methods. The type could for example have the primary key optional if it is auto-incremented, while the returning type T has it required. With this PR, the TInsertType is also reused by Table.update() and Collection.modify() through `UpdateSpec<TInsertType>` to compute all possible keypaths that could be updated without risking the compiler having to traverse an endlessly recursive path in case T is a complex object with recursive properties. See also our discussion around this in the original PR #1764. --------- Co-authored-by: Nick <[email protected]>
Merged through #1897 |
Completion the the Pattern of TInsertType. I found a Type recursion issue with my complex class types dealing with the updating and hooks. Specifically the (UpdateSpec) since it generically looks at the passed in Type recursively it suffers from circular references on complex class types. So I completed the pattern by passing along the TInsertType respectively. if specifying the TInsertType it should be used universally to prevent leaky abstraction making it's way into Dexie Entity processing
Nick