-
Notifications
You must be signed in to change notification settings - Fork 7
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
How would I write “either” and “withDefault” decoders? #4
Comments
Hi! Thanks for the kind words and for using the library! I'll do my best to answer your questions, feel free to correct any misunderstands I have of your problem. If I understand your first case correctly, I think it is as simple as: const decodeFoo = record({
id: string,
slug: fields({ id: string, slug: optional(string) }, ({ id, slug }) => slug ?? id),
}); Kind of annoyingly, you have to specify You were on track with the As far as your hypothetical So yes I belive you are spot on that For reference, this would be an example implementation: const just = <T>(x: T): Decoder<T> => (_: Pojo) => x Not quite sure how thîs would best interact with missing fields in a record, though. Look forward to hearing your feedback! |
Perfect, much appreciated! The And d-oh, you’re right about if (!value.hasOwnProperty(key)) {
if ((decoder as any)[optionalDecoder]) {
return [key, undefined];
}
throw `Cannot find key \`${key}\` in \`${JSON.stringify(value)}\``;
} So if the key is missing, the “default value decoder” doesn’t even get a chance to run. Even if I cheated a bit and tagged the default value decoder as |
So how about something like this in the implementation of if (!value.hasOwnProperty(key)) {
// If this is an optional decoder, it’s fine to return undefined
if ((decoder as any)[optionalDecoder]) {
return [key, undefined];
} else {
try {
// Not an optional decoder, but maybe the decoder can handle undefined?
// This is what should make things like `union(boolean, just(false))` work.
return [key, decode(decoder)(undefined)];
} catch (_) {
// No, it can’t, so there’s nothing left but to throw
throw `Cannot find key \`${key}\` in \`${JSON.stringify(
value,
)}\``;
}
}
} I would try to implement this and send a PR, but I’m not sure how the test suite works. I think the package makes a lot of sense and it would be nice to have a standard Jest test suite – to make it easier to contribute and maintain. It’s an extra dependency, but only at develop time. Would you consider changing the tests to a Jest test suite? (It’s perfectly fine if not, for whatever reasons.) |
Very interesting suggestions! I have another TypeScript library where the test suite, which can be found here: https://github.com/tskj/typescript-calendar-date/blob/master/specs/index.tests.ts, uses a combination of Jest and fast-check, which I have been very happy with. However, I haven't been able to figure out how to set it up for this library, since what I would like to test is if the TypeScript compiler infers the expected types (while Jest is a runtime JS thing). However I will take another look at this, as good tests are obviously very important, to have faith in the library. As far as your concrete suggestion goes, I think maybe the cleaner solution then is to drop the entire However, this now doesn't allow any mechanism I can see for people to fail their decoders if the key is missing, and that is what they want. So I am on the fence about making this change, but I might be missing something here. On the other hand, maybe making this distinction at all between missing keys and explicit In either case, another way to do what you want might be something like this: record({
foo: optional(boolean, x => x ?? false),
}) I have been considering whether more of the combinators (than just field and fields) should support this kind of "continuation passing style" for allowing further transformations. I'm guessing a lot of people not too familiar with parser combinators or similar might find this more intuitive to understand than the Look forward to hearing your opinions on both topics. |
I’ve split the test suite discussion into another issue to keep things tidy. As for the second point, I did not realize that And as for the third point (transforming the decoded values), I have to say that’s something that I was looking for when starting to use the library. It feels quite natural and feels like a good middle ground when you want to transform the value a bit, but don’t want to introduce a new custom decoder. (Let’s maybe extract this branch of the discussion into a separate issue, too?) |
Good idea splitting the issues up, I have created another issue here. It seems to be as simple as deleting that if check, and I'm all for it! It almost doesn't seem like a breaking change, which is very good. The only reason I'm slightly hesitant is that we would be removing any possibility for the user to detect missing keys (or more precisely, distinguishing missing keys from undefined keys). While I agree that this is a pathological usecase, there might (or maybe not?) be APIs out there which encode different meanings in missing keys vs undefined keys. I just think we should provide some escape hatch in this pathological case juust in case, but I might be overthinking this. |
It’s a quite complex topic, isn’t it? To recap: There are three possible ways to express some notion of a “missing value” – nulls, undefined and missing keys. Javascript (unfortunately) really does make a difference between all these. And we already have ways to express them here – the Our major use case is JSON, where there’s no explicit undefined, so we only have nulls and missing keys. And there’s a legitimate reason to mix those. For example if I want to send a PATCH request to change an object, there‘s a difference between setting a key to null (“I want to clear this field”) and leaving a key out (“I don’t want to touch this field”). Let’s take an example: interface Contact {
email?: string
phone?: string
} If I want to clear the {"email": null} In JavaScript, that could be either const decodeUpdateRequest = record({
email: optional(union(string, nil)),
phone: optional(union(string, nil)),
});
// As inferred by decodeType<typeof decodeUpdateRequest>
type UpdateRequest = {
email: string | null | undefined; // string: set new value, null: clear value, undefined: no change
phone: string | null | undefined;
} Does that make sense? Now, I’m still looking for a legitimate use case where the object being decoded makes a difference between missing and undefined keys. It can’t be JSON, but since JavaScript makes the difference, it makes sense to have the escape hatch, maybe for decoding values from other sources? Would it make sense the repurpose the current tagging mechanism used for optional decoder? Say we introduce a |
Thanks for clearing this up for me, I actually didn't know
Do you want to submit a PR for this? If not I will give it a go shortly. |
Note that (I think) this is actually different from I'm currently using |
Ah, yes, there are two possible default value semantics: 1) use default when the main decoder doesn’t succeed, or 2) use default when the value is missing. |
I think I will get on #6 shortly, then! Seems like people think it's a good idea and would be valuable. As for |
It might be, generally speaking, though personally, I prefer (2) for my use cases. If the server unexpectedly changed the API I'd rather get an error than blindly using the default value. |
Ah, gotcha. I think we are in agreement! Let me nuance my position a bit: I think it makes the most sense for Another argument for only catching exceptions (failling decoders) and not undefined or null values, is that I would rather not have the library mandate a sentinel "fail" value on the user's behalf. You would then have to answer the question, should it be By the way props on your current solution, that's pretty ingenious! Had to take a few moments to reason through how it works to convince myself it actually behaved as expected. |
Heh, thanks :) It does sound like we agree, yeah |
Hi! Thank you for this perfect library, I’m thrilled to have something better than just
JSON.parse
and praying. I’ve got two use cases I don’t know how to go about, though.“Either” field decoder – I am decoding a record and want to decode a
slug
field that contains a string. That’s easy, but if the field’s missing, I want to use theid
field instead:How can I do that without some kind of postprocessing? I’ve tried a few things to make something like
either(field("slug", string), field("id", string))
work, but since the field decoder is treated as special in therecord
decoder, I didn’t get anything working.And the second case is something like
withDefault(boolean, false)
decoder. If theeither
decoder works, would this simply beeither(boolean, just(false))
with a helperjust
decoder that just returns a value?Thank you!
The text was updated successfully, but these errors were encountered: