Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Treat JSON types more literally #26552

Closed
3 of 4 tasks
qm3ster opened this issue Aug 20, 2018 · 23 comments
Closed
3 of 4 tasks

Treat JSON types more literally #26552

qm3ster opened this issue Aug 20, 2018 · 23 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@qm3ster
Copy link

qm3ster commented Aug 20, 2018

Search Terms

import json literal type specific type

Suggestion

When importing a JSON file, strings and numbers are typed as string and number rather than the string or number literals in the file.
Also, array literals are imported as T[] instead of [T1, T2, T3] tuples.

Since the JSON is almost an object literal, I believe it makes more sense to type it more specifically.

I have faced most of my issues with the former, but I believe as much information as possible should be provided, which means tuples instead of arrays.

At least the former shouldn't be particularly breaking as the types are replaced by subtypes, but of course can be breaking if typeof reflection is used to jam less specific types into it.

Use Cases

In a number of cases, JSON imported are expected to conform to a schema that includes specific enums. Please see example.

Examples

import * as eventSchema from './src/event.schema.json'
import { compile } from 'json-schema-to-typescript'

const typings = compile(eventSchema, 'Event')

Currently, this will not compile, because

Type string is not assignable to type "string" | "number" | "boolean" | "object" | "integer" | "array" | "null" | "any" | JSONSchema4TypeName[] | undefined

This forces skipping the type checking of this completely statically available, and valid, object:
const typings = compile(eventSchema as any, 'Event')

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript / JavaScript code 🤔
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. new expression-level syntax)
@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Aug 20, 2018
@RyanCavanaugh
Copy link
Member

Since the JSON is almost an object literal

It is except where it isn't, and when it isn't, it really isn't. You wouldn't want to require in a JSON file of customer data and see "Jason Bobsworth" | "Alice Freelan" | "Jane Costo" | "Roger Mister" | "Ted Laker" | "Frank Frankly" | "(600 more...)" in type information.

There are some good tools like https://quicktype.io/ that can configurably generate types from JSON; I'd recommend using something like that to "write down" the shape of the JSON you want to see.

@qm3ster
Copy link
Author

qm3ster commented Aug 20, 2018

I'm talking specifically about raw JSON files using Typescript's native JSON typing.
I am in fact using json-schema-to-typescript (as seen in example) for times when I am working with dynamic JSON, I was specifically talking about the static case.
Can you clarify why I wouldn't want to see "Jason Bobsworth" | "Alice Freelan" | "Jane Costo" | "Roger Mister" | "Ted Laker" | "Frank Frankly" | "(600 more...)" in type information?

@qm3ster
Copy link
Author

qm3ster commented Aug 20, 2018

It does seem like a much deeper problem indeed, since even within the same file, even this:

const string = 'text'
const object = { string }

results in a

const object: {
    string: string;
}

type.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Aug 20, 2018

Consider if you did something like this:

import customers = require("./customers.json");

// Error: "Bob Bobbily" is not assignable to "Jason Bobsworth" | "Alice Freelan" | "Jane Costo" |...
customers.push({ name: "Bob Bobbily" });

You might want to read https://blog.mariusschulz.com/2017/02/04/typescript-2-1-literal-type-widening or #20195

@qm3ster
Copy link
Author

qm3ster commented Aug 20, 2018

Extremely informative link, thank you.
I see there are indeed some downsides.

@bali182
Copy link

bali182 commented Sep 24, 2018

Is there any solution for this use case (other than casting)?

sample.json

{
  "type": "frog"
}

sample.ts

import sample from './sample.json'

type AnimalType = 'frog' | 'cat' | 'dog'
type Animal = { type: AnimalType }

const a: Animal = sample // compile error

What I'm trying to do is testing generated types (let's say Animal in the example) against hand written JSON samples. My issue with just simply casting:

const a = sample as Animal

Is that it can mask some other "type incorrectness" in the generated types.

@qm3ster
Copy link
Author

qm3ster commented Sep 26, 2018

@bali182 That's literally the problem I had. It seems that this option is not currently supported.
If you just want to test your typings, you might get away with using the JSON as an object literal:

type AnimalType = 'frog' | 'cat' | 'dog'
type Animal = { type: AnimalType }

const a: Animal = <%= sample_json_as_text %>

for

type AnimalType = 'frog' | 'cat' | 'dog'
type Animal = { type: AnimalType }

const a: Animal = {
  "type": "frog"
}

which would pass.

@qm3ster
Copy link
Author

qm3ster commented Jan 22, 2019

Being bitten by this over and over again, sometimes have to resort to generating something like this from the JSON:

interface _Common {
  "json": { "literal": ["here"] }
}
type RecursiveReadonly<T> = T extends object
  ? { readonly [K in keyof T]: RecursiveReadonly<T[K]> }
  : T
type Common = RecursiveReadonly<_Common>
export { Common }

This types all the arrays as tuples as well, which is pretty nice.

Would be much nicer though if I just had an option to import the JSON module like this.

@Yolepo
Copy link

Yolepo commented Apr 17, 2019

This do the trick :

const data: Data = JSON.parse(JSON.stringify(Your_Data_Json));

@m-b-davis
Copy link

m-b-davis commented May 29, 2019

@RyanCavanaugh Since we can now assert const - is there any possibility this will be re-considered in the future?

Something like:

import Schema from './schema.json' as const;

Would be amazing.

@slorber
Copy link

slorber commented Jun 24, 2019

I'm wanting the same: ability to import a json as a const literal and get appropriate union types.

My json file is a configuration, which contains a list of available app locales. I want the union type instead of just "string"

Edit: created a feature request as it did not seem to exist yet: #32063

@ht4963
Copy link

ht4963 commented Oct 19, 2019

I'm running into exactly the same issue: need to check my test data stored in JSON files against auto-generated types from schema. Would love to see a solution for this.

@mikeselander
Copy link

I'm not sure why this has been closed, but throwing my hat in for desiring a fix for this. Reading JSON more literally into string types would be a significant improvement to be able to put configs into JSON.

As an example, the WordPress Gutenberg project is moving towards a JSON registration schema for multiple reasons. The list of available category options could and should be tightly limited to the available options. However, due to this bug, we could never enforce a proper category list which effectively breaks TS linting of the file for anyone wanting to use TS when creating Gutenberg blocks or plugins.

@nojvek
Copy link
Contributor

nojvek commented Apr 18, 2020

I know this issue is closed but having a const json importer would be very useful.

When you inline the json inside a .ts file e.g const json = {“hello”: “world”} then world is treated like a const, but if you import, then it’s a string.

Some typescript way if going from const to strong via a typecase makes sense for those who want existing behavior.

With existing json behavior any times that use discriminated unions break because type comes back as “string”.

Typescript authors, how you do propose I validate json to a type?

@mscottnelson
Copy link

I've been trying to work on a fix for some of these issues here: https://github.com/pabra/json-literal-typer. If your use-case is relatively straightforward (limited special characters, no escape characters in string literals), then it may satisfy some needs. Would love to have this built-in to the language, but hopefully this will be helpful to some in the interim.

@kamok
Copy link

kamok commented Sep 20, 2020

I ran into this when one of the functions, to which I'm passing my json into into, is typed.

import myconfig from './config/

some_function(myconfig)

I get type errors saying that some_function expects a value to be a string literal, but instead gotten type string.

This defeats the purpose of the typing on some_function. This is not ideal when the json I import is massive (why we move it to another file in the first place). I end up coercing it with as ConfigOptions, but this also defeats the purpose of the type checking since I can now pass in incorrect data to the function.

I did come up with a solution, which is to not use .json and rename the file to a .ts. Then, I did a named export on the config, which I typed as the provided ConfigOptions. This preserves ability to not mess up my values that I pass in.

Here's the example:

export const config: GrantOptions = {
  "defaults": {
    "protocol": "http",
     ...
  }
}
...
import { config } from './config/grant-config';

app.use(grant(config))

@slifty
Copy link

slifty commented Jul 20, 2022

Wanted to chime in here to agree with the limitations of the current implementation and provide another use case.

I'm using ajv to create type guards for my types that are powered by a Type-validated-JSONschema. This is really handy because it's possible for ajv to throw type errors if the schema (and therefore the type guard) does not match the Type definition.

This all works fine if defined inline:

export interface Foo {
  bar: number;
};

const validatedFooSchema: JSONSchemaType<Foo> = {
  type: 'object',
  properties: {
    bar: {
      type: 'integer'
    },
  },
  required: ['bar']
};
export const isFoo = ajv.compile(validatedFooSchema);

However, I want to define my json schema in an external json file so that I can reference it in other places (e.g. in an OpenAPI spec). I cannot do this with the current TypeScript json import implementation, since this schema is not interpreted as the required string literals but as generic string types:

fooSchema.json

{
  "type": "object",
  "properties": {
    "bar": {
      "type": "integer"
    },
  },
  "required": ['bar']
}

Foo.ts

import fooSchema from '../fooSchema.json';

export interface Foo {
  bar: number;
};

const validatedFooSchema: JSONSchemaType<Foo> = fooSchema;
export const isFoo = ajv.compile(validatedFooSchema);

^ validatedFooSchema errors with:

Types of property 'type' are incompatible.
        Type 'string' is not assignable to type '"object"'

@Antony74
Copy link

Antony74 commented Sep 2, 2022

This is a genuine challenge, especially when it comes to working with ajv schemas, but it is not a json import problem. We can trivially convert json to ts by prefixing export default. Notice the same thing still happens.

fooSchema.ts

export default {
    type: 'object',
    properties: {
        bar: {
            type: 'integer',
        },
    },
    required: ['bar'],
};

Foo.ts

import Ajv, { JSONSchemaType } from 'ajv';
import fooSchema from './fooSchema';

const ajv = new Ajv();

export interface Foo {
    bar: number;
}

const validatedFooSchema: JSONSchemaType<Foo> = fooSchema;
export const isFoo = ajv.compile(validatedFooSchema);

^ validatedFooSchema errors with:

  Types of property 'type' are incompatible.
    Type 'string' is not assignable to type '"object"'.

What a pity we can do things with literal types within .ts files that we can't do between files.

i.e. the following does successfully compile and run:

import Ajv, { JSONSchemaType } from 'ajv';

const ajv = new Ajv();

export interface Foo {
    bar: number;
}

const validatedFooSchema: JSONSchemaType<Foo> = {
    type: 'object',
    properties: {
        bar: {
            type: 'integer',
        },
    },
    required: ['bar'],
};

export const isFoo = ajv.compile(validatedFooSchema);

@qm3ster
Copy link
Author

qm3ster commented Sep 5, 2022

@Antony74 for your thing, try

export default {
    type: 'object',
    properties: {
        bar: {
            type: 'integer',
        },
    },
    required: ['bar'],
} as const

You could do

import { JSONSchemaType } from 'ajv'
import { Foo } from 'Foo.ts'
export default { /* ... */ } as JSONSchemaType<Foo>

but I suppose that may defeat the point for you.

This issue is specifically about importing .json files without modification or additional tooling.

@Antony74
Copy link

Antony74 commented Sep 5, 2022

@qm3ster Yes you're right, the as const does the trick with .ts thanks, making the problem just to do with .json where there is no equivalent trick.

(you're also right about as JSONSchemaType defeating the point, which is to check (at compile time) rather than assume that the given schema is compatible with the type Foo).

@qm3ster
Copy link
Author

qm3ster commented Sep 5, 2022

@Antony74 it shouldn't assume anything, it would still conflict.
as JSONSchemaType<Foo> doesn't blindly cast conflicting types, only as any as JSONSchemaType<Foo> does that.
as const as JSONSchemaType<Foo> may be stronger though, not sure about your case.

If you want to typecheck but export the most literal type and not potentially partially type-erased, you could also go with

const x = { /* ... */ } as const
const _: JSONSchemaType<Foo> = x
export default x

@malmod
Copy link

malmod commented Jun 28, 2023

Please consider implementing loading JSON definitions as const!

This is clearly a missing feature. In our case, configuration options are loaded from a server (they usually don't change daily), so the dev runtime fetches the config and voila. However, to have better string typing, now we must convert the JSON into a fake ts file with a script. Importing JSON resources as const by default would solve this.

spalladino added a commit to AztecProtocol/aztec-packages that referenced this issue Sep 15, 2023
Adds missing path property to struct types and removes "as unknown"
casts for going from json files to ABI types. Note that the cast is
still needed since ts loads JSON files with values understood as
strings, so without the cast the type checker complains about
`visibility` being a string rather than an `ABIVisibility`. And changing
the visibility type from an enum to a union type doesn't cut it, see
[this ts issue](microsoft/TypeScript#26552)
for more info.
spalladino added a commit to AztecProtocol/aztec-packages that referenced this issue Sep 15, 2023
Adds missing path property to struct types and removes "as unknown"
casts for going from json files to ABI types.

Note that the cast is still needed since ts loads JSON files with values
understood as strings, so without the cast the type checker complains
about `visibility` being a string rather than an `ABIVisibility`. And
changing the visibility type from an enum to a union type doesn't cut
it, see [this ts
issue](microsoft/TypeScript#26552) for more
info.
@Ayfri
Copy link

Ayfri commented May 14, 2024

Any update ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests