gcanti / io-ts-types

A collection of codecs and combinators for use with io-ts
https://gcanti.github.io/io-ts-types/
MIT License
311 stars 40 forks source link

`t.record(NumberFromString, ...).is(value)` always returns `false` #165

Open mrdziuban opened 2 years ago

mrdziuban commented 2 years ago

When using NumberFromString along with t.record, the is method of the resulting codec always returns false, e.g.:

t.record(NumberFromString, t.string).is({ 0: "test" })   // false
t.record(NumberFromString, t.string).is({ "0": "test" }) // false

Is there any way I could get this working without defining a custom codec? Thanks in advance!

P.S. I wasn't sure if I should open this issue here or in the base io-ts repository so let me know if you'd like me to re-open it there.

EricCrosson commented 2 years ago

If we look at the code for NumberFromString^1 we see the type-guard is implemented to check that the input value is a number. In general, for the AfromB codecs, the type guard checks for type A, which matches the decoded type A. In other words, type guards on these type-converting codecs are seemingly intended to be used on decoded values.

It seems like your first attempt in the snippet above would work, but Records are where things get tricky. First, note that t.record really only shines with the specific codec t.string in the domain (keys); even branded strings produce unacceptably-poor type inferencing (widens to any). This is probably why the domain is fixed as string (non-parameterized) in io-ts v3.

Technically in bare TypeScript Records accept a domain of string | number | Symbol but even this is a bit of a stretch of the truth -- in the node repl we can see

$ node                      
Welcome to Node.js v16.13.1.
Type ".help" for more information.
> const a = { 4: 'more' }
undefined
> a
{ '4': 'more' }
> a['4']
'more' 

that number keys are coerced into strings. This is probably why your first attempt failed, because the number you started with did not exist by the time your codec's type-guard ran.

If you need arbitrary types for your domain, consider using an es6 Map.