gcanti / io-ts

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

Struct type with literals not inferred correctly in Decoder #656

Closed vecerek closed 2 years ago

vecerek commented 2 years ago

🐛 Bug report

Current Behavior

import * as D from "io-ts/Decoder";

/*
  Inferred as:
  Decoder<unknown, { _tag: string }>
 */
const incorrect = D.struct({
  _tag: D.literal("Some")
});

Expected behavior

I expect the type to be inferred as:

type A = Decoder<unknown, { _tag: "Some" }>;

Reproducible example

import * as D from "io-ts/Decoder";

const incorrect = D.struct({
  _tag: D.literal("Some")
});

Suggested solution(s)

I tried changing struct in the Decoder to:

export const struct = <A extends Record<string, Decoder<unknown, any>>>(
  properties: A
): Decoder<unknown, { [K in keyof A]: TypeOf<A[K]> }> => pipe(UnknownRecord as any, compose(fromStruct(properties)))

to better align with the implementation of struct in Codec, where this inference issue does not happen.

However, the type is then inferred as:

type A = Decoder<unknown, { _tag: unknown }>;

I suspect the implementation of TypeOf to be at fault there but I am not familiar with the theory behind Kleisli types, so it's become difficult for me to debug any further.

Additional context

I stumbled upon this after upgrading to TypeScript 4.8 where it caused build errors. My workaround to this issue was to cast the string to a const:

const correct = D.struct({
  _tag: D.literal("Some" as const)
});

Your environment

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

This issue should be reproducible on the master branch. I don't know if the inference ever worked.

Software Version(s)
io-ts master
fp-ts 2.9.5 (the one used on master at the time of reporting the issue)
TypeScript 4.6.2 (the one used on master at the time of reporting the issue)
gcanti commented 2 years ago

@vecerek thanks for the bug report, I'm investigating. Looks like literal doesn't work as expected inside a struct with typescript@4.8 (while it works with typescript@4.7 and earlier versions)

gcanti commented 2 years ago

Looks like changing the literal signature from

export const literal: <A extends readonly [S.Literal, ...ReadonlyArray<S.Literal>]>(
  ...values: A
) => Decoder<unknown, A[number]>

to

export const literal: <A extends readonly [L, ...ReadonlyArray<L>], L extends S.Literal = S.Literal>(
  ...values: A
) => Decoder<unknown, A[number]>

solves the issue, PR here https://github.com/gcanti/io-ts/pull/657