nvie/decoders

Typescript error with required properties

stevekrouse opened this issue · 9 comments

It seems like AllowImplicit is confusing Typescript into thinking that required properties are optional.

interface A {
  a: string;
}

const aDecoder: Decoder<A> = object({
  a: string
});

Output:

Type 'Decoder<{ a?: string; }, unknown>' is not assignable to type 'Decoder<A, unknown>'.
  Type '{ a?: string; }' is not assignable to type 'A'.
    Property 'a' is optional in type '{ a?: string; }' but required in type 'A'.ts(2322)

Live example: https://codesandbox.io/s/nostalgic-sinoussi-lkrq1?file=/src/index.ts

I ran into the same trouble in one of my projects (TS 4.4.3, decoders 1.25.4). Oddly enough, in a different project (TS 4.3.5, decoders 1.25.1), all is working as expected. Installing the same versions of TS and decoders into the project with the problems didn't solve them.

nvie commented

I would love to get this fixed. Do you have any clues about what might be causing this behavior of the AllowImplicit type here? Can you perhaps put together a list of reproduction steps to go from an empty directory to getting this error? Would be much appreciated! 🙏

I'll try to come up with a minimal reproduction.

I narrowed it down to the strict setting in tsconfig.json.

  • when "strict": false (the default, when not set), the problem exists
  • when "strict": true the problem is gone

Steps to reproduce

Step 1: Create npm project

cd /tmp
mkdir repro-decoders-750
cd repro-decoders-750
npm init -y
npm i -D typescript@4.4.4
npm i decoders@1.25.4

Step 2: Create source file

Create file repro.ts:

import { object, string } from "decoders";
import type { Decoder } from "decoders";

export interface Person {
  name: string;
}

export const personDecoder: Decoder<Person> = object({
  name: string,
});

Step 3: Create tsconfig.json files

Create tsconfig.bad.json:

{
  "compilerOptions": {
    "moduleResolution": "node",
    "module": "es2020",
    "lib": ["es2020", "DOM"],
    "target": "es2020",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "strict": false
  }
}

Create tsconfig.ok.json:

{
  "compilerOptions": {
    "moduleResolution": "node",
    "module": "es2020",
    "lib": ["es2020", "DOM"],
    "target": "es2020",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true
  }
}

Step 4: Demonstrate behaviour

Demostrate different behaviour:

npx tsc --noemit -p tsconfig.ok.json
# no output = no compiler errors = all ok

npx tsc --noemit -p tsconfig.bad.json
repro.ts:8:14 - error TS2322: Type 'Decoder<{ name?: string; }, unknown>' is not assignable to type 'Decoder<Person, unknown>'.
  Type '{ name?: string; }' is not assignable to type 'Person'.
    Property 'name' is optional in type '{ name?: string; }' but required in type 'Person'.

8 export const personDecoder: Decoder<Person> = object({
               ~~~~~~~~~~~~~


Found 1 error.

See difference in config:

diff -u tsconfig.*
--- tsconfig.bad.json	2021-10-29 15:37:58.000000000 +0200
+++ tsconfig.ok.json	2021-10-29 15:38:00.000000000 +0200
@@ -8,6 +8,6 @@
     "esModuleInterop": true,
     "skipLibCheck": true,
     "forceConsistentCasingInFileNames": true,
-    "strict": false
+    "strict": true
   }
 }
nvie commented

Awesome, thx! Will try to take a look somewhere next week.

nvie commented

Thank you for this bug report @stevekrouse and thanks for these amazing reproduction steps @djlauk 🙏 !

I've narrowed the source of the bug down further and found that the strictNullChecks flag is the real culprit (which is part of the family that strict will enable).

https://www.typescriptlang.org/tsconfig#strictNullChecks

"When strictNullChecks is false, null and undefined are effectively ignored by the language. This can lead to unexpected errors at runtime."

This sounds really scary and I'm looking to understand when you would ever want to use this, especially if you are already in the center of the Venn diagram of users interested in using TypeScript and decoders 😄

I'm currently trying to figure out if I want to dive in and see what's causing this and see if I can somehow make it compatible with that setting (it would be nice it it would Just Work™ no matter how your TypeScript project is configured of course!), or… if I should just recommend to always use strictNullChecks: true when using decoders.

Thoughts? Are you deliberately not using strictNullChecks for some reason?

@nvie Thanks for looking into the matter!

To me, it would be sufficient to add it to the documentation, I guess. I can try and come up with a PR for that.

I encountered this problem when adding decoders to a toy side project whilst trying out SvelteKit. Turns out their default tsconfig.json doesn't enable strict mode.

My best guess about people not wanting TS's strict mode, is to ease migration from JS. In the case of Svelte / SvelteKit the destructuring shorthand is conveniently used for function parameters. In JS that looks nicely terse (e.g. have a look at the get function in this example here); in TS that terseness only works without strict mode. (If you enable strict mode, TS complains about params implicitly being type any.)

nvie commented

Yeah, you're absolutely right about that. I've made a fix this morning, see #769. This is now published as v1.25.5 - thank you both so much! 🙏

Thanks!!