io-ts: Handle missing properties correctly
š Bug report
Current Behavior
const T = t.type({ a: t.unknown })
assert.strictEqual(T.is({}), true) // property 'a' is missing, but the type check still passes
Expected behavior
const T = t.type({ a: t.unknown })
// property 'a' is missing but must be present
assert.strictEqual(T.is({}), false)
// property 'a' is present, and undefined is a valid value for unknown
assert.strictEqual(T.is({ a: undefined }), true)
// property 'a' is present, and null is a valid value for unknown
assert.strictEqual(T.is({ a: null }), true)
Similarly, in vanilla TypeScript:
type T = { a: unknown }
// won't compile
const T1: T = { }
// good
const T2: T = { a: undefined }
// good
const T3: T = { a: null }
Reproducible example
See above
Suggested solution(s)
Will submit a pull request.
Additional context
Your environment
Software | Version(s) |
---|---|
io-ts | master |
fp-ts | 2.0.0 |
TypeScript | 3.7.4 |
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 1
- Comments: 26 (9 by maintainers)
@gcanti Iāll submit the pull request now so you can take a look as it. I can always update the PR with whatever changes make sense.
Regarding the idea of this being a breaking change, I think itās more of a bug fix - but in this case people may have been inadvertently relying on the bug behavior. I donāt think itās entirely black and white what to do from a semver perspective for cases like thisā¦
Getting ahead of myself perhaps, but if you do decide to take the change ā which I hope you do since I spent 2 solid days testing it! ā I would be happy to write something up for people on how to migrate to the new behavior if you think that would help.
Ah⦠@gcanti the extra context helps a lot.
JSON indeed canāt directly represent undefined values and strips out those properties in
stringify
. I can see why that may lead you to consider treatingundefined
(andvoid
which is just an alias forundefined
) in a special way.That said, you can use a replacer function in
JSON.stringify
to preserve undefined properties by substitutingnull
or a value like"VALUE_UNDEFINED"
forundefined
:https://stackoverflow.com/questions/26540706/preserving-undefined-that-json-stringify-otherwise-removes
Then, when parsing, you can use a reviver function in
JSON.parse
to substituteundefined
- or any other value you like - for your placeholder.As you can see by the stack overflow question, this is something that people are used to dealing with since itās just how JSON works.
Anyway,
type.decode
is called on objects, not on strings - so itās called after any potentialJSON.parse
. That means that the caller is responsible for providing the correct pre-decode object representation.I really donāt think it should be up to
type.decode
to correct for potential mistakes made by the caller in their object representation.For example, letās say I have a data source that sends me both Users and HomelessUsers. Users must have an address property, but the address value can be undefined. HomelessUsers must not have an address property. Thatās how I expect to distinguish between them.
Letās also say Iāve already parsed the data to the correct object representations ā or maybe I didnāt need to parse the data because my data source is creating and sending me objects directly in the same process.
Hereās some sample code:
Note that according to the data, Ann is a
User
who happens not to have a defined address, and Joe is aHomelessUser
.When I used
User.decode()
on these values, it succeeded for both ā and in the process it also converted Joe from aHomelessUser
to aUser
which is not what I wanted or expected to happen. He magically gained anaddress
property and became a regularUser
, which could have all kinds of implications in my application ā and is really subtle and un-documented behavior.Now, maybe we fix
is
so that if we call User.is on Ann pre-decode it will returntrue
, but on Joe it will returnfalse
. Are you saying that I have to callis
before I calldecode
if I want to be safe and not accidentally convert Joe to a regular user? Even if I was expecting the data source to only send me regular users and itās their mistake?If Iām used to how Typescript types behaves, then Iām sure to be confusedā¦
By the way, I mainly use
decode
as a smart constructor - for example to take data published in an event by one part of my application and to turn it into a domain type in another part of my application.io-ts
helps to enforce the contract between different loosely-coupled parts of the application - and ensures that my domain objects can only contain valid data.io-ts
is awesome for these kinds of cases - not just i/o š