Branded ID type constructor function
I like that IDs have been changed from classes to branded strings in 0.17, and I also like that they're treated as opaque (not constructable directly). But while upgrading, I just came across a situation in which I think it would make sense to be able to obtain an
ID<"myTable">
from a string
—when serializing the ID to the DOM, e.g. as the value
attribute of a <select>
element. The example in my code, snipped verbatim:
Perhaps it would be nice, for scenarios like these, to offer a function which tries to create a branded string by validating that the string is the correct format (length/content) for a Convex ID? I guess that would just mean validating that it's a UUID (v4). Something like:
Of course I can coerce the string to the type I'd like, but this alternative could be nicer. Just a data point and a thought 🙂7 Replies
Yeah totally -- we're currently not exposing enough information to clients to be able to tell whether an ID is in a particular table (leaning on
db.normalizeId
and argument validators like v.id("brandPartners")
instead). But would love for us to support something like this from client code someday -- thanks for the concrete example!Out of curiosity - are there scenarios where you'd want an ID validator with weaker guarantees - e.g. that just validate that it looks like an ID, but doesn't validate it's for a specific table? On the backend the distinction is pretty serious b/c things like
db.get
will access whatever table the ID is for, regardless of the type at compile-time.Yeah @ian, that's what I had in mind with my suggestion above—validation just that it looks like an ID. While obviously not as good, the current state of things is that I need to assert that an arbitrary string is an ID, and a validation check that at least it looks like it's an ID would still protect against some bugs! In fact, I'd guess that asserting that a non-ID string is an ID is the more likely failure mode for most people's code than asserting that an ID for one table is rather an ID for another (but even if not, the value is still there).
I wonder what the right verbage would be useful to avoid folks over-estimating the security of it. It would be unfortunate if they exposed a vulnerability b/c they used the wrong validator server-side. Maybe it could just validate that it's an ID but make it clear the table is a type? I like your suggestion, since
Id.fromString<"myTable">(id)
has the table not as a parameter. It's odd to say fromString
when it is still a string subtype. Would something like checkId<"brandPartners">(id)
make it clear?I think technically
string
isn't a subtype of something like string & { _brand: "myTable" }
(rather the opposite) but I take your point that it could still be confusing, especially because of the peculiarities of how branded types function in general. You could always lean towards a dangerous-sounding name to ensure confusion in a safer direction, if there is any 😄. Something like coerceId<"myTable">(id)
? Not sure
Defaulting the type parameter to any
(or unknown
?) could perhaps also make it more clear that if you pass something in there, it's just an assertion
E.g.
Another scenario I'm encountering now—storing a Convex ID in localStorage
What I've done is cast it to the Id type when reading it, and validating it when passing it to a cloud function, since that's where it has the risk of reading from the wrong table
Is the ask to have a way to validate something coming from
localStorage
as being of an ID format and determining the table? Or is there a usecase I'm missing?I'm doing all of those things as well! In this case, I was using
localStorage
to store search filters which I want to persist between page visits (the ID is a filter parameter). When the page loads, I want these "last-used" filters to be used as parameters to the query, unless they are absent or malformed, in which case I just want to allow the query to use its own default filtering parameters. For other types of filter values which I have in localStorage
, it's possible to use a library like zod
to parse out the value and ensure it is a valid parameter (or not) and then to bail or proceed accordingly. But my concern with casting was that I'm more likely to pass along an invalid ID to the query and then have the query fail, or else have to handle this localStorage
concern at that layer, as well. Which isn't as ideal, because I really want this localStorage
caching feature to give up instantly as soon as there's any hint that some data might not be correct, so that I don't have to version it as I make changes to the app.
Does that make sense?
I still think it's probably not a huge deal, especially because you can only really ever be so sure that some arbitrary string you receive is a valid ID for some table without involving the database. But that was the thinking that led to the mention!