gcanti / io-ts

Runtime type system for IO decoding/encoding
https://gcanti.github.io/io-ts/
MIT License
6.7k stars 328 forks source link

Surprising acceptance of various inputs #690

Open NicoleRauch opened 1 year ago

NicoleRauch commented 1 year ago

🐛 Bug report

Current Behavior

For educational purposes, I played around with the available codecs and encountered these (in my opinion erroneous) cases:

1) t.UnknownArray accepts ["ABC", 123] which is a tuple, not an array.

2) t.UnknownRecord accepts const n: number = 7; const x: Record<number, string> = {[n]: "n"}; although the documentation claims that it accepts only Record<string, unknown> i.e. string keys.

3) t.record(t.string, t.number) accepts const n: number = 7; const x: Record<number, number> = {[n]: 888}; although the key is a number, not a string.

4) t.strict({ a: t.number, b: t.string }) accepts {a: 777, b: "Hello", also: "x", and: 123} although it should reject objects with extra fields. The same holds true when t.strict is replaced by t.exact(t.type(...)).

5) t.partial({a: t.string}) accepts {a: undefined} even when "exactOptionalPropertyTypes": true is set in tsconfig.json

Expected behavior

Ideally all of the above codecs should reject the presented example objects.

For 2) and 3) I am not sure whether these are really distinguishable types in TypeScript? (If I change the Record type to Record<string, ...> I don't get a typing error.) If they are not distinguishable, the documentation should reflect this.

Reproducible example

See above.

Suggested solution(s)

See above.

Your environment

Which versions of io-ts are affected by this issue? Did this work in previous versions of io-ts?

Software Version(s)
io-ts 2.2.20
fp-ts 2.13.1
TypeScript 4.9.5
ragnese commented 1 year ago

For 1, TypeScript tuple are arrays, in multiple senses. First, tuples are arrays in a literal sense in that JavaScript doesn't have "tuples". So, at runtime, anything that was a "tuple" in your TypeScript code really is a JavaScript Array at runtime. Second, even in TypeScript's type system, tuples are a subtype of Arrays. So, any function that accepts an array of a certain element type will also accept tuples of any arity as long as the elements in the tuple are compatible with the element type of the array in the signature.

For 2, this is also consistent with how JavaScript and TypeScript work (try passing a Record<number, string> to a function that accepts Record<string, string> and vice versa). Though, I suppose the documentation should be changed to not be misleading.

Same with 3. It's unfortunate, but JavaScript object keys are never really numbers. They always get converted to numbers. For example, this is valid: const a = ['a', 'b', 'c']['0'] // variable a now = 'a'. So, I don't think there's anything to do for this one, either.

4 is somewhat debatable, but I don't think it should work the way you're describing. TypeScript is primarily structurally typed, which means that types/interfaces describe a minimum requirement of a type. So, a TypeScript type will accept an object with "extra" fields as correct. The current io-ts behavior is consistent with that philosophy, and all strict/exact do is to actually strip those extra fields for us. It would be strange and inconsistent with the rest of TypeScript to reject objects with extra fields.

5 is the only one that I agree is troublesome. There are other issues that discuss io-ts's take on undefined vs. "optional" properties. The official stance of io-ts is/was that they will be treated the same. I disagree with this decision, but I'm just a user.