json-schema-to-typescript icon indicating copy to clipboard operation
json-schema-to-typescript copied to clipboard

additionalProperties can create invalid typings

Open awlayton opened this issue 5 years ago • 13 comments
trafficstars

The following schema:

{
  "properties": {
    "s": { "type": "string"}
  },
  "additionalProperties": {
    "properties": {
      "foo": {"type" : "string"}
    }
}

produces this typing:

/* tslint:disable */
/**
 * This file was automatically generated by json-schema-to-typescript.
 * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
 * and run json-schema-to-typescript to regenerate this file.
 */

export interface FooSchema {
  s?: string;
  [k: string]: {
    foo?: string;
    [k: string]: unknown;
  };
}

Which produces this tsc error:

foo.d.ts:9:3 - error TS2411: Property 's' of type 'string' is not assignable to string index type '{ [k: string]: unknown; foo?: string; }'.

awlayton avatar May 08 '20 00:05 awlayton

Hey there! In this case, it's not clear to me what the expected behavior is here. It really is a contradiction that s must be both a string, and an object with a key foo.

Instead, consider expressing this using "oneOf", to make it clear that you want a shape that's one or the other, and not both. Something like:

{
   "oneOf":[
      {
         "properties":{
            "s":{
               "type":"string"
            }
         },
         "additionalProperties":false
      },
      {
         "additionalProperties":{
            "properties":{
               "foo":{
                  "type":"string"
               }
            },
            "additionalProperties":false
         }
      }
   ]
}

Which generates:

export type Demo =
  | {
      s?: string;
    }
  | {
      [k: string]: {
        foo?: string;
      };
    };

bcherny avatar May 09 '20 20:05 bcherny

The schema is not unclear though? s is always a string and any other properties are objects with a foo key.

awlayton avatar May 09 '20 20:05 awlayton

This is a fine type for what I want, which is exactly what my schema means:

/* tslint:disable */
/**
 * This file was automatically generated by json-schema-to-typescript.
 * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
 * and run json-schema-to-typescript to regenerate this file.
 */

export type FooSchema = {
  s?: string;
} & {
  [k: string]: {
    foo?: string;
    [k: string]: unknown;
  };
}

awlayton avatar May 09 '20 20:05 awlayton

Maybe JSONSchema and TS have different semantics here:

export type S = {
  s?: string;
} & {
  [k: string]: {
    foo?: string;
    [k: string]: unknown;
  };
}

let s: S = {s: 'x'}
/*
Error TS2322:
Type '{ s: string; }' is not assignable to type 'S'.
  Type '{ s: string; }' is not assignable to type '{ [k: string]: { [k: string]: unknown; foo?: string | undefined; }; }'.
    Property 's' is incompatible with index signature.
      Type 'string' is not assignable to type '{ [k: string]: unknown; foo?: string | undefined; }'.
*/

What you're trying to express is: "the field s has shape X, and every field that is not s has shape Y". The problem you're running into is you can't express "every field that is not s" in TS's type system -- you can express "every string field", which includes s.

bcherny avatar May 09 '20 20:05 bcherny

My typing work for my case which is handling input from non ts code (see this) and I need to make sure that the ts code treats it properly.

It is true there is a bit of an issue with typescript and this sort of thing, but there is certainly a better solution that what it compiles to now.

awlayton avatar May 09 '20 20:05 awlayton

I feel this is definitely a bug. My schema is valid and unambiguous. This should either be documented as a shortcoming or it should compile to something at least valid in ts. If you wanted it to compile to your union thing that would be better than producing an error at least.

awlayton avatar May 09 '20 20:05 awlayton

I agree. The current behavior of producing TypeScript files that fail to compile is broken. It either should fail during this tools runtime or produce a type that captures as much of the schema meaning as possible.

abalmos avatar May 09 '20 22:05 abalmos

While I’m not sure it’s technically feasible to guarantee that the result typechecks, maybe we can start by doing better in this case in particular.

bcherny avatar May 09 '20 23:05 bcherny

Here’s the TS issue that blocks this one: https://github.com/microsoft/TypeScript/issues/17867. I’d love ideas for a workaround if someone has one, but I’m not sure there’s a great way to do this today.

bcherny avatar May 09 '20 23:05 bcherny

Similar issue: generated types are too common.

For example, generated type is

type A = {
    x: number;
    [k: string]: unknown;
}

Type of x does not matter here. TS will use unkown and allow to set anything in x:

const test: Omit<A, "y"> = {
    x: "bla",
}

https://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=10&pc=7#code/C4TwDgpgBAglC8UDeAoK6oA8BcUB2ArgLYBGEATgNxoYDaA1rgM7DkCWeA5gLq4F708AewDueagF8UKAMZC8LKMAgtcAeSJtgAHhgAaKACIQhgHwJkNdDiMkANgENDelFJTKWAOkxA

It would be handy to have an option that ignores additionalProperties: true. TS allows unspecified properties in runtime anyway.

SlNPacifist avatar Jul 28 '20 15:07 SlNPacifist

Hi! Any chance of reviewing PR #383 which might close this issue?

fabrykowski avatar Apr 06 '22 10:04 fabrykowski

Hi,

I see a long discussion also in the pr. In my case, I have a schema that generates something as:

{
        "."?: PackageExportsEntry | PackageExportsFallback;
        [k: string]: PackageExportsEntry | PackageExportsFallback;
}

Whilst imho the simple correct rendering should be:

{
        "."?: PackageExportsEntry | PackageExportsFallback;
        [k: string]: PackageExportsEntry | PackageExportsFallback | undefined;
}

The error is due to the fact that the index type is not optional, so making it in | undefined fix the issue. The JSON schema I used is the official schema for package json.

Not sure if this would fix all the cases mentioned in this issue and in the pr, nor if my proposed fix is actually correct. But it could make sense, since accessing a field whose name is not known, it could truly create a undefined value, so at least it is safe to mark it as possibly undefined.

Regards

FStefanni avatar Aug 08 '22 06:08 FStefanni

I raised #477 to cover that case @FStefanni but it was closed as a duplicate of this issue. Commenting here as well just in case comments on closed issues aren't monitored.

Personally I don't see why this issue duplicates the one that we're having with the package.json schema. That is, I literally just want strictIndexSignatures to do exactly what the docs say that it does and "[a]ppend all index signatures with | undefined".

The fact that it doesn't may be related to this issue, and it may resume working as documented when this issue is resolved, but if the intended behaviour of the strictIndexSignatures flag is really as simple as what's in the doc, it doesn't really make sense to me to block resolving that very narrowly-scoped problem on a resolution for this wider problem.

benjamincburns avatar Aug 09 '22 01:08 benjamincburns