[GH-ISSUE #20] Allow better types for DataReference and DataSnapshot #15

Closed
opened 2026-05-23 08:36:32 -06:00 by gitea-mirror · 7 comments
Owner

Originally created by @futurGH on GitHub (Oct 3, 2022).
Original GitHub issue: https://github.com/appy-one/acebase-core/issues/20

DataSnapshot#val() always returns any, despite the fact that, given the existence of TypeMappings binding, a user will often know at compile time what type will be returned. I have a few suggestions for type-level changes with no runtime impact, apart from the 3rd option, listed roughly in order from least change to acebase-core to greatest internal change. I'd be happy to submit a PR for any of them, just wanted to get feedback first.

Option 1 (Generic-as-cast)

Workbench Example

const snapshot = await db.ref<MyClass>("foo/test").get();
const value = snapshot.val();
//    ^? - MyClass
  • Simplest internal implementation, no runtime impact on JS
  • Equivalent to snapshot.val() as MyClass
  • Potentially controversial because it can hide the fact that there's no type checking going on, especially for developers new to AceBase and/or the codebase

Option 2 (User declaration merging)

Workbench Example

declare module "acebase" {
    interface BindingMap {
            "foo/*": MyClass;
    }
}
const snapshot = await db.ref("foo/test").get();
const value = snapshot.val();
//    ^? - MyClass
  • Moderate option; allows users to declare mappings on the type level with the same syntax used for TypeMappings#bind
  • Can default to any for users not using type mappings
  • May be unintuitive depending on user's knowledge of TypeScript

Option 3 (bind as assertion function)

Workbench Example

db.bindType("foo/*", MyClass);
const snapshot = await db.ref("foo/test").get();
const value = snapshot.val();
//    ^? - MyClass
  • "Just works" if user binds types with TypeMappings
  • Unfortunately would require bind to be implemented as a top level method of AceBase, as current TypeScript limitations prevent the functionality used from working if it's nested in a member class
  • Would be a breaking change for all users
Originally created by @futurGH on GitHub (Oct 3, 2022). Original GitHub issue: https://github.com/appy-one/acebase-core/issues/20 `DataSnapshot#val()` always returns `any`, despite the fact that, given the existence of `TypeMappings` `bind`ing, a user will often know at compile time what type will be returned. I have a few suggestions for type-level changes with no runtime impact, apart from the 3rd option, listed roughly in order from least change to acebase-core to greatest internal change. I'd be happy to submit a PR for any of them, just wanted to get feedback first. ### Option 1 (Generic-as-cast) [Workbench Example](https://www.typescriptlang.org/dev/bug-workbench/?#code/PTBQPYAIEEGECiAhGBlBUAyBJJAlGPATSjgHkARBSGkKAAQDMBLAGwFMA7AQwFt2AXFG4BjdgCNuAZ3YA6AC5TQAE3YjW3AE7so66VNhik0nQG9QUS1G2MAPABUoAXmGcAngD4AFAAdu8gAshKXlNZk4AcwBKIQp-bjx2RnZtTjEHDwBuUABfFTUNbV0NKQM4+QSklK50+w8ocysoCPZ5LxioAAVNAHteZhkHNx92VG5k8u5UHh8pAJ75DI9c0HlhnXt1sYn46e5Z+cW65waLKwA3blZ2oXsV0HYADx8ezXkGw3ZjGSg80FowHQAKroPCkSjUCBQ6BMNhcPiCKAAVxkmgUSmYvBebw+MCMJl+UEYvV4UAARKIJCYydlVHoiiIepwQlBlOIhHiviZsozme8pDMTtwAO7cZjvNmyGxeMmMHo9YCSTRkqKyFptKKgXksy6sJE6FwC-ayXXtbJ0KwAPQA-FAALSuNz-THY96mT7fHTSKDbdicz2E4l9cmyYCUyQyGn-PSlKAAWTccBKBlMeTphR02veaxGvooSCEvv93K1TJZOdG41GgpcIrF2a2VfzUqStgTSf03ll8uA8nYIRVata7VLfKgFd9ADUrvqThOq3sfCarmbIE0bfb44nk0A) ```ts const snapshot = await db.ref<MyClass>("foo/test").get(); const value = snapshot.val(); // ^? - MyClass ``` - Simplest internal implementation, no runtime impact on JS - Equivalent to `snapshot.val() as MyClass` - Potentially controversial because it can hide the fact that there's no type checking going on, especially for developers new to AceBase and/or the codebase ### Option 2 (User declaration merging) [Workbench Example](https://www.typescriptlang.org/dev/bug-workbench/?#code/PTBQPYAIEEGECiAhGBlBUAyBJJAlGPATSjgHkARBSGkKAAQDMBLAGwFMA7AQwFt2AXFG4BjdgCNuAZ3YA6AC5TQ8gJ4AHdlDzs1rUewDqbACZxuAJ2NSAPABUo7AB7yuVqFPnnmnAOYA+KABeKHsnF043AAMAEgBvb0Z2cygkdkYAe3N2AF8AKjiEpNhGF3NsyNAoKqgAfigY2O1dfSNWUwsra1SMrL9suI8vX37GnT0xVvbLGxgSpL6K6qghW2V1TRh5F141eQA1blYAV3YAMXN03gAFbnkACzsHZ1cpd09vHwAaKABpdhVXmEXlAANb-dKMFLeYwfACy3DUQVB4MhSGhcIRAWCfwBT3CbkKyRxUEq1TqxKBEVeYJUEKhEQxiNJSyqdVCzypWjGLRMZmm1hxAWZLKWdTRDN88LUAG0cQBdYUiqpCTjsABuRUV1RV6s1Sx1GvMAG41hooAdjmcLtdbg92fjXoMPljYFt2Dt9ocTudLjd7nYApSCZxEskLScSaKoNLw+w5XjgdLVYb41rWcJOCpI0qhLHs8qoMmkibQMZ2CI9FkoBXpK9bOtUNxEjAxEhpJpYsKsoxHkHHe9fH4ABRqW0rACUKwbTfYFFu3G0oa4YmssZ9Nv9tj8flA2VL5crmhrUjr08Sc-kC7SSWX7ADUE7Sx87HkQ8nUCu1uYMjsZ9n89QHg1CkO50nkAMdz3MsayrY9Tw0RtzwAoCQLA+9H2qNVDjfFZd1AUBvFKRh9HpGFJQRB891AJw1EyeQH1IxlvnrBCZxbdg2xkKBshLWgwDoABVdA8FIShqAgCToCYNguD4QQoCOGRzAUJRmB2OiGPYzjNGyKBGGtKAACJ9EkGRDJNaDD2rdJOA8KBjHEIQtPbE0RBsuypCApFuAAd24Zh6Ic2RuyHQyMnSYAXA8Qzx1kZ9X3HUA3Ns+isMtJFPIRWQ0rfI0oDobhM3wtTaPMejYhCP9nK43T9MuIzZGAEz23M-cYM0Xh0mMI4OAapqxFM9hDIfYVCKSYixEY8jEQwpVqjC9IItyQyhAACVsWFMAQDh+E4eQTTmpZDNyYBJHMFbYHMcxuBUawjk4EFOHSHzOD8A7qj3KCDwsI93Po1RWPPJAp0B9hqvYVy-qgAH2EQ9h1s27b3S4T1LTWjatp2lGkSHXz-P+v8KCQYK0lC8LIvYaLYvit9Ypy8dIZS6Gqqum7Yyc1nbvux7ntenG8YC5nQaJknGFCqL5FOiwYril9aey7CGaSqHCpUbApFQeQ2FYK50hPZhxA4IRVf5vzBZhuGRZCwyns4YAnG-cJ5BlmnxzpxWjSAA) ```ts declare module "acebase" { interface BindingMap { "foo/*": MyClass; } } const snapshot = await db.ref("foo/test").get(); const value = snapshot.val(); // ^? - MyClass ``` - Moderate option; allows users to declare mappings on the type level with the same syntax used for `TypeMappings#bind` - Can default to `any` for users not using type mappings - May be unintuitive depending on user's knowledge of TypeScript ### Option 3 (`bind` as assertion function) [Workbench Example](https://www.typescriptlang.org/dev/bug-workbench/?#code/PTBQPYAIEEGECiAhGBlBUAyBJJAlGPATSjgHkARBSGkKAAQDMBLAGwFMA7AQwFt2AXFG4BjdgCNuAZ3YA6AC5TQzXgAcA9gCd5UAN5QAKgE9V7ALLdVq5pwDmZVfObrOUqAF8ojTet5QARKIS0uz+ANyg8ibsUHjsqqxBAOpsACZw3JqpUgA8BlDsAB7yXNlQUvKaNrYAfFAAvIYFxaVuAAYAJLo2jOyaUEjsjFrs7gBUXT19sIwlmu5toFDLUAD8UJ26cQnJaRlZuYPDmuw17l0VVXbnW-GJYims6ZnZOTCzfWeLK1BCBpHRWDyEpqeQANW4rAAruwAGI+XgABW48gAFjklit8kUSpwypdqgAaKCY5ZIGypaoWVTNXFlOIiLSpHIEuzEqGcADWnHUAHdODViaSoABpdhGNw41pQTni9SMAYUqmWBoyuUK8l45WqUB1RpiiW06VTfoGkk-dZmqV4tyyozyxVauzUqAAMnKlWqwotTWt9Luuye+1eBrq3p+K3WmspzssAG0DQBdcMR35QTjsABu0xTaYz2f6wqE+b6ESipigEOhcIRyLRGJ+2JaNo9V1sQp+0e1RpbDKZLM9bKgHO5fIFutVMGB7FBVZh8N8dfRBmJXdjqjqfrcJsrkJh5sjUDjc-YiZ7ZTjJc0Z9z624nCMB9TQhPT7zWdLoFAqXYIkSJygP9pDcYxTFQbhehgMQkBCPRhXEJU7CkIRdHcCIfgQvFQPYHIrWbMpER8UxtCMA1iRPGoAApc1UFFUSEMi3x+ctBHTdheSgSjZG4zJbGQ4QHwASgaOoTw7VMoHURxnFcVY-mialrDsBwnBcNwAB9hzxIYbHYVJhUEoRgL6RQoDRZhtzcfRMJjPihD7LJcPFci91ODx0JWE5GDyc83FZWpKNotE-kMwxonA3oKBRbg4l6E5ODEHITwXJE6LyYlzKkON-Bs6opH8RMahqCJ3G-X9-xiICpBA8KIPYKL5BioY+i4RKDDqXRhVsdh5Eo0LCN8CycOwiL6ui1AeFUKRUXUeQ8iK0BSpYsKwLqhruAmyxptm+bVU6n5M0hPq-kWr8ig0bQ9BW9hRqg9gYJkDwv1oMA6AAVXQPBSEoagID+6AmDYLg+FYqEZE0BQlBUC6dH0O6HpiTxvF8AIgkkGRwi-Kq3DMIxYXUdQ4ESaq9FK7GoFxmDNCJ4DSeen8gIAxlXB0VJxCEeGQgiNmFGiKRZBsyj-GGdRgDGfxiVx-HCeJqRBIiZmKnKSbVW4XluGYVnxFkLyhZF4ASgqfxBNkbresE0BFZ0Q7q1VKRJtkG2+rCKA6HvIxnuhrRYeu27oNgpGEQCWRgDRkJMbKxnKrUnQWNGigkHk1bIP9mQy1qyKkAFilsL1gmxYlim8YJmnqvlgFk-qrObNz-wxmASRNELynMlLuWFZjsyM7hAmTyEKWS9l1VKLVjXY+7hOdaGPPRcN+RjdNnq+pNp3y6trvK6pvui6ptvh9HzWN5utas91-w54bzIF7N5fHaOtfO-d7ApFQJxWFYRF1Gq5hxA4IyH33urQ+ccT5T0YELHknBgBFAsrieeJsb6CRXvfMIQA) ```ts db.bindType("foo/*", MyClass); const snapshot = await db.ref("foo/test").get(); const value = snapshot.val(); // ^? - MyClass ``` - "Just works" if user binds types with `TypeMappings` - Unfortunately would require `bind` to be implemented as a top level method of `AceBase`, as current TypeScript limitations prevent the functionality used from working if it's nested in a member class - Would be a breaking change for all users
Author
Owner

@appy-one commented on GitHub (Oct 4, 2022):

Thanks, that all looks great! I'll try out the code from the workbenches, great stuff!

<!-- gh-comment-id:1267380503 --> @appy-one commented on GitHub (Oct 4, 2022): Thanks, that all looks great! I'll try out the code from the workbenches, great stuff!
Author
Owner

@appy-one commented on GitHub (Oct 17, 2022):

Sorry it's taking a while to get back to you, I've been working to port all last js bits of all acebase packages to typescript. I'm almost finished, so I'll take another look at this once everything is working well and published. Thanks again!

<!-- gh-comment-id:1280982411 --> @appy-one commented on GitHub (Oct 17, 2022): Sorry it's taking a while to get back to you, I've been working to port all last js bits of all acebase packages to typescript. I'm almost finished, so I'll take another look at this once everything is working well and published. Thanks again!
Author
Owner

@futurGH commented on GitHub (Oct 17, 2022):

No worries! Let me know what you think whenever you can find the time to take a look and I'd be happy to get to work on an implementation.

<!-- gh-comment-id:1281008491 --> @futurGH commented on GitHub (Oct 17, 2022): No worries! Let me know what you think whenever you can find the time to take a look and I'd be happy to get to work on an implementation.
Author
Owner

@Bubz43 commented on GitHub (Oct 19, 2022):

I've personally been using something similar to option 2 and it seems to work quite well.

Minimal Example Playground

Throw any object interface at it and you are good to go, though you are lying about the shape of the DB data and have to keep in mind you need some other way actually guarantee correctness on initial creation/migrations.

<!-- gh-comment-id:1284502086 --> @Bubz43 commented on GitHub (Oct 19, 2022): I've personally been using something similar to option 2 and it seems to work quite well. [Minimal Example Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgQQMYFMBCBDAzuuAXzgDMoIQ4BybDAIz3SoG4AoUSWRVuOAEWwxsAJXQl0UdADsMAGh79BI9DCjB0AN2wAbAPJgYwCFNzzeAoQGUp2MLgAWEGPOJkK1Wugb4AtKmhMbBzQ8EgA4iqycJZg2sDwruSUVDAAnmDoPuK4MCysrGkZcBEwmKkACoL2ADyVMPZw6AAeMNIAJrhwOWpSAOZRAGJJAHxwALzFKtUKQxRm0bHxtVVRVAD0VMPzSN3AqDAAXHCqAK4EhKzDbOxSrVAknnAAKunobRbK4pIy6NVPwwpmq0pB04LoQEsPqIvtI5NR8Lk4AAfagnMBtQRMZHUXoqKjYqioezAbRtfEoqhgE4OTbcXgIgAUWm0ZyOTwAlEdyklgPg-q93kpoRJYb9-lcFGiMa0GQpeETsH10Lgjrs+o0Wu1OgBrdCpCAkZ5y3hwAD8cFE-igbWqav6zwA2rr9YangBdbFSE7abQAk0mtka4GgiB0ABW6H2xt45sqsGAOj+fv9cDZ805cG5FF5YoFULEIp+SYlClxMAZGazEL5LwygqsNjAlkcMGLbF4ZYZEAMRhMpqO+dU6mZ+kMxlwlZ5NbzSmstmbTjb+XlxNJtSDWq6Q76wwZYCqXIztbe+ZhRZKZTqNXKUXF114VIcFbZM6Ewu+GGm-qeG5BnUt0A2naUTAFIXxwAAqqM5oAHLGDB3raNgdDaL8UGpnAUiaBICgSg+1L2LK-rMqyzy-qCAHWra272qB4HoeaEEYVhGgSOmXJTr8xrHvWnyFp+0ZkUCm6UUBNEgWBEiQdBcBwVICE+shqHVOhRwsTh-rJhKFw3HcDwYM8r7YHOTYtkm5GdOCkKzo2DhOKszLktQkgkLSCAKMyz7PO2cAuS+danvxYrafkqBIbgnQ8WgWCMNUfCYBZcChhG+yjO5vCSNgbTGNoqRwCctx5pgRzRTg+D3nA-gmKc+zQAybR0CVGBleg7J0ia9S8gAdAVhQnglEwNT5OkZWIfyJcBcAAGrjJMpQVFUfxRPFwy7vu9Rsm16UmpIMAnFAUjHMSuA9YVAWYF1Ll7lUbV4PlUjalIEAAO6HXdBWPS9h3CX+02Cea6lQIJ-knkKBYfr8U14UQy6+RATgVu1o17QdR3db1RWXfD8DvQ9T2vXAd08YFENxZg0MXDpoF6Y8HzeOgHztdSEgqhakaAV+JrAcaSA2CA6CqjRzBwPzEXYLirOidRPT2kgrQtILMtEKMFy8NDJDw0c228AwUBa4JDAAF5qScIB0BIDpuj5vCEMNbA6XgqQyKQBX7L2+X4DxDKePTTUxfgW0KFVORwA1s1Yc9hl1qVsV04wHy7j7jDshVwfwCA2CpObEH4FA0KzQ1l1iAyVDM1AuBrAAjAATAAzFQKdB+O8AaxAOBQDghv54NdBFyQJet2sutD9ghsN6nzceyz3eh73V2l7nuDj031WYegz05xIM-YM92DxFP5fQl1j6Ebz2D80cVAwevkG51QUSi7g4vKlrxCEI38qTxHACyypP7i29d77wjpvPOYhjRdSJCSNoJdH7PyXuyCBJ8GRyyBJfAAEugH0EB8TvwnqvVAmVWhtFAQXOexcAAGZcK4ABIEAgNzkfZ0hAKEf0qpPKUmISGMLELNHee94CEPQFw0BPV0SYhQZhc+As4AACJr7PSkjBaRsj36wzTmvZ6v8xa4hMnwoB8Af5-2fkfTsbCNFGJ0QQCYlj-7oBMl1TyjcV4h0KBAGehd55l0NlXOuy8LhAA) Throw any object interface at it and you are good to go, though you are lying about the shape of the DB data and have to keep in mind you need some other way actually guarantee correctness on initial creation/migrations.
Author
Owner

@futurGH commented on GitHub (Oct 19, 2022):

Totally agree, thanks for the example! With compile-time–only type checking like TypeScript's, there's always the risk that data won't line up with expectations, especially when it's being moved across file system boundaries as in the case of a DB. I've been using a version of option 2 in my own projects as well, and it's definitely best accompanied by a runtime function to assert that the received type is indeed what you're expecting.

<!-- gh-comment-id:1284569632 --> @futurGH commented on GitHub (Oct 19, 2022): Totally agree, thanks for the example! With compile-time–only type checking like TypeScript's, there's always the risk that data won't line up with expectations, especially when it's being moved across file system boundaries as in the case of a DB. I've been using a version of option 2 in my own projects as well, and it's definitely best accompanied by a runtime function to assert that the received type is indeed what you're expecting.
Author
Owner

@futurGH commented on GitHub (Feb 3, 2023):

Hope you don't mind a quick bump @appy-one — I haven't been using AceBase as of late so if there's interest I'd love to submit a PR while the implementation is somewhere in my brain 🙂

<!-- gh-comment-id:1414760533 --> @futurGH commented on GitHub (Feb 3, 2023): Hope you don't mind a quick bump @appy-one — I haven't been using AceBase as of late so if there's interest I'd love to submit a PR while the implementation is somewhere in my brain 🙂
Author
Owner

@appy-one commented on GitHub (Feb 7, 2023):

@futurGH yes please, you are more than welcome to! I think option 1 would be the best starting point for now, maybe option 3 at a later stage!

<!-- gh-comment-id:1421468645 --> @appy-one commented on GitHub (Feb 7, 2023): @futurGH yes please, you are more than welcome to! I think option 1 would be the best starting point for now, maybe option 3 at a later stage!
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: github-starred/acebase-core#15
No description provided.