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

import ConstJson from './config.json' as const; #32063

Open
5 tasks done
slorber opened this issue Jun 24, 2019 · 74 comments
Open
5 tasks done

import ConstJson from './config.json' as const; #32063

slorber opened this issue Jun 24, 2019 · 74 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@slorber
Copy link

slorber commented Jun 24, 2019

Search Terms

json const assertion import

Suggestion

The ability to get const types from a json configuration file.

IE if the json is:

{
  appLocales: ["FR","BE"]
}

I want to import the json and get the type {appLocales: "FR" | "BE"} instead of string

Use Cases

Current approach gives a too broad type string. I understand it may make sense as a default, but having the possibility to import a narrower type would be helpful: it would permit me to avoid maintaining both a runtime locale list + a union type that contains the values that are already in the list, ensuring my type and my runtime values are in sync.

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. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

Links:

This feature has been mentionned:

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jun 26, 2019
@Hawkbat
Copy link

Hawkbat commented Jul 3, 2019

This would be extremely useful for adding direct support for JSON schemas to TypeScript. This can technically be accomplished with generators right now, but it would be so much more elegant to be able to use mapped types (for example, https://github.com/wix-incubator/as-typed) to map an imported JSON schema to its corresponding TypeScript type. It isn't currently possible to use this approach with a JSON import since the type property of each schema object will be a string instead of 'boolean' | 'string' | 'number' | ....

@Porges
Copy link
Member

Porges commented Jul 20, 2019

FWIW I just tried to do this and used the exact same syntax that the issue title uses, if that's any indication of how intuitive it is 😁

@dontsave
Copy link

I just tried the exact above syntax also. Const assertion is a fantastic tool and it would be incredible to have the ability to assert static json files at import

@mikeselander
Copy link

I added a note to #26552 and now realize that I put it in the wrong place, so copying it over here :D

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.

@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.

@Kingwl
Copy link
Contributor

Kingwl commented May 13, 2020

// CC: @DanielRosenwasser @RyanCavanaugh

How about syntax import const ConstJson from './config' and limited for the json modules.
I'm happy to work on this if it could be accept.

@m-b-davis
Copy link

m-b-davis commented May 13, 2020

@Kingwl I think this syntax could be slightly more confusing than the alternatives. It could look similar to the import name when viewed in a sequence of imports. It would be good to get some a view on what the preferred syntax would be for everyone.

1 - import const myJson from './myJson.json';
2 - const import myJson from './myJson.json';
3 - import myJson from './myJson.json' as const';

Personal view:

#1 - As mentioned above, not sure it's optimal due to distance from import name
#2 - Perhaps too similar to const foo = 'abc'. I think this would at first pass look more like a variable assignment than an import
#3 - This is more similar to the behaviour we have for as const so I would vote for this as the one that fits current design the best.

Thoughts? Have I missed any alternative syntax options?

@m-b-davis
Copy link

Also happy to work on this if it progresses!

@TheMrZZ
Copy link

TheMrZZ commented May 17, 2020

I'm for option #3. This one looks similar to the current "as const" syntax:

const x = {...} as const

It makes it more intuitive. Definitely a killer feature for config-based code if Typescript adopts it.

@parzhitsky
Copy link

parzhitsky commented Aug 27, 2020

As great as this suggestion is, how should TypeScript interpret the type of property in the original comment?

{
  "appLocales": [ "FR", "BE" ]
}
  1. Sealed tuplet: readonly [ "FR", "BE" ]
  2. Tuplet: [ "FR", "BE" ]
  3. Array of custom strings: ("FR" | "BE")[]
  4. Array of arbitrary strings: string[] ← the current one

I think, there's no way for TypeScript to know the desired level of strictness without developer explicitly specifying it somehow, — and it looks like this will have to be done per each file, rather than once in tsconfig.json

@parzhitsky
Copy link

I think I'm gonna answer my own question 🙂

The const keyword in as const is not much about inferring the types literally, as it is about declaring the value as immutable, read-only, sealed. That in turn helps to infer the types more literally, without the worry about being too specific.

With this in mind, it would be intuitive and expected to set the output of *.json modules to be read-only, forbidding any mutable operation on them. This would make it work just like it currently is working with runtime objects and as const assertion.

@ThomasAribart
Copy link

@parzhitsky I think most would agree on the 1st suggestion, as it is coherent with the present as const statement, and as it is the narrowest option (other types can easily be derived from it if needed).

@daniellwdb
Copy link

daniellwdb commented Nov 9, 2020

This is getting even more valuable after Variadic Tuple Types since we can create more types based on the json values, think of json files used for localization so we can extract interpolation. Any thoughts on implementing this yet?

@mscottnelson
Copy link

For the last half-year or so I have been forcing this behavior by having a somewhat kludgy pre-compile step that reads in a config.json like {"thing":"myVal"} and exports it as a config.ts like export const Config = {"thing":"myVal"} as const; and use the resulting type definition on the imported json. (previously I needed to do a lot more, prepending readonly everywhere to get the desired array behavior, but at some point that all became unnecessary). It is very helpful during development!

Configuration will likely vary at runtime and thus the content of the json import cannot be known; nevertheless, a as const compiled json delivers on all of TypeScript's primary design goals. I can report that having used it to wrangle over-sized configuration json, it has been invaluable in:

  • enforcing valid configuration defaults
  • inspecting (with "debugger"-like precision) current configuration values while working on code that consumes it
  • providing quick semantic help with regards to the shape of configuration subtrees when that shape is not yet defined in a TypeScript definition (ie on the consumption side).

That is to say, from a pragmatic perspective, import config from './config.json' as const does most of the things that I find TypeScript most helpful for.

@lukeapage
Copy link

@RyanCavanaugh you set this as "Awaiting more feedback" - it has 110 thumbs up and a load of comments from people who would find the feature useful. Can it be considered now or at least the tags changed? Or does it require more people to add to the emoji's ?

@ThomasAribart
Copy link

@RyanCavanaugh This feature would be very helpful for json-schema-to-ts. You could define and use JSON schemas on one side (API Gateway, swagger... whatever!), and use them in the TS code to infer the type of valid data. Less code duplication, more consistency, everyone would be happier!

@gabro
Copy link

gabro commented Dec 22, 2020

Chiming in to add another use case: with the new recursive mapped types + string transformations we would like to parse the ICU syntax in translation files in order to extract the params needed to translation strings.

Here's a simplified example:

type ParserError<E extends string> = { error: true } & E
type ParseParams<T extends string, Params extends string = never> = string extends T ? ParserError<'T must be a literal type'> : T extends `${infer Prefix}{${infer Param}}${infer Rest}` ? ParseParams<Rest, Params | Param> : Params
type ToObj<P extends string> = { [k in P] : string } 

const en = {
    "Login.welcomeMessage": "Hello {firstName} {lastName}, welcome!"
} as const

declare function formatMessage<K extends keyof typeof en>(key: K, params: ToObj<ParseParams<typeof en[K]>>): string;

formatMessage('Login.welcomeMessage', {firstName: 'foo' })                  // error, lastName is missing
formatMessage('Login.welcomeMessage', {firstName: 'foo', lastName: 'bar' }) // ok

Playground link

This currently requires as const on the translations object, which we can't do because it lives in a json file.

@teppeis
Copy link

teppeis commented Jan 26, 2021

Related: #40694 Implement Import Assertions (stage 3)

@SimonAlling
Copy link

SimonAlling commented Mar 3, 2021

I also found my way here because I ran into type errors when trying to use a JSON module in a somewhat strongly typed context. In addition to the already mentioned use cases, being able to import JSON modules "as const" would allow one to do this:

import { AsyncApiInterface } from "@asyncapi/react-component"
import React from "react"

import schema1 from "./schema1.json"
import schema2 from "./schema2.json"

function dropdown(schemas: readonly AsyncApiInterface[]) {
  return (
    <select>
      {schemas.map(schema => <option>{schema.info.title}</option>)}
    </select>
  )
}

dropdown([ schema1, schema2 ]) // type error at the time of writing

EDIT: One would even be able to statically express for example that the current schema must be one of the existing/supported/listed schemas (and not just any schema):

import { AsyncApiInterface } from "@asyncapi/react-component"
import React from "react"

import schema1 from "./schema1.json"
import schema2 from "./schema2.json"
import schema3 from "./schema3.json"

type Props<Schemas extends readonly AsyncApiInterface[]> = {
  schemas: Schemas
  currentSchema: Schemas[keyof Schemas]
}

class SchemaList<Schemas extends readonly AsyncApiInterface[]> extends React.Component<Props<Schemas>> {
  render() {
    const { currentSchema, schemas } = this.props
    return (
      <ul>
        {schemas.map(schema => (
          <li style={schema === currentSchema ? { backgroundColor: "#66BBFF" } : undefined}>
            {schema.info.title}
          </li>
        ))}
      </ul>
    )
  }
}

<SchemaList
  schemas={[ schema1, schema2 ] as const}
  currentSchema={schema3} // Would be a (much appreciated) type error because it's not in `schemas` (unless schema3 is exactly identical to one of them).
/>

@Tommos0
Copy link

Tommos0 commented Mar 16, 2021

Possible workaround (if your file is called petstore.json):
echo -E "export default $(cat petstore.json) as const" > petstore.json.d.ts

Now import as normal to get the literal type.

@apancutt
Copy link

Possible workaround (if your file is called petstore.json):
echo -E "export default $(cat petstore.json) as const" > petstore.json.d.ts

Now import as normal to get the literal type.

To avoid "The expression of an export assignment must be an identifier or qualified name in an ambient context." (TS2714) errors, use:

echo -E "declare const schema: $(cat petstore.json); export default schema;" > petstore.json.d.ts

@sebastian-fredriksson-bernholtz
Copy link

@matthew-dean I think you're missing a few very important points. Most importantly, satisfies and as const fulfil very different purposes. The purpose of as const is to assert (cast/coerce) the type of a literal value into the narrowest possible type. satisfies is for validating that the type of a value is a subtype of another type without making any type assertion (cast/coercion)!

Lets look at satisfies in some detail:

  1. It's better to discuss this issue in terms of 'type narrowing" and "type widening" rather than the much broader "type coercion".
  2. satisfies does not technically do any type coercion - or type narrowing/widening for that matter. The entire purpose of the satisfies operator is to verify that the type of a value conforms - is a subtype - to another wider type without widening the type of the value . It most definitely is not intended to do type narrowing!

So if satisfies doesn't do type narrowing, what is going on in your example?

  1. A literal value in TypeScript - unlike variables, etc - isn't really of a single specific TypeScript type. While it is of a specific runtime type - and the runtime type corresponds to a TypeScript type - the value can be interpreted as either the primitive type or the literal type.
    Eg. The literal string "hello" is of runtime type string, but can be interpreted as both the TypeScript primitive type string and the string literal type "hello".
  2. When a variable is assigned to without a type annotation, TypeScript infers that the variable is of the same type as the type of what it's being assigned. Without any further information TypeScript will interpret all literal values as their primitive types rather than their literal types.
  3. However, if a literal value satisfies a literal type, Typescript can only interpret it as the literal type, since interpreting it as the primitive type would fail the type verification.

So now we can explain your example despite satisfies not technically doing any type casting:

let foo = { variant: "primary" } 
// can be interpreted as either primitive or literal type, is interpreted as primitive

let bar = { variant: "primary" } satisfies { variant: "primary" } 
// invalid if interpreted as primitive  - 'string' not assignable to '"primary"' - is interpreted as literal

I'd be interested in knowing how you think as const

doesn't really successfully coerce all the types that you might expect into the shape you actually want.

The only thing that I could think of is that you don't want it to be readonly. However, with the proposed as const import and the existing satisfies operator you could do whatever you want with the structure:

const foo = {
  variant: "primary",
  invariant: "green"
} as const
// equivalent of importing as const, type is { readonly variant: "primary"; readonly invariant: "green"; }

const bar = {
  ...foo,
  variant: "secondary",
  additional: "stuff"
} satisfies {invariant: "green", variant: string, additional: "stuff"}
// bar is of type  { variant: string; invariant: "green"; additional: "stuff" }

If you're talking about something similar to what the original commenter had in mind it can also be achieved through derivation as @parzhitsky mentioned:

const foo = {
  appLocales: ["FR","BE"]
} as const
// type { readonly appLocales: readonly ["FR", "BE"]; }

type Locale =typeof foo["appLocales"][number]
// type "FR" | "BE"

const locale: Locale = "FR"

@matthew-dean
Copy link

matthew-dean commented Jun 3, 2023

@sebastian-fredriksson-bernholtz

I would buy your argument that satisfies isn't doing any type coercion except for two points:

let foo = 'one' satisfies 'one' | 'two'

In the above example, foo does satisfy the type given, yet the type of foo is still string. If one is a value within an object, the behavior flip-flops, and type coercion occurs.

Second, in the TypeScript documentation, it explicitly says this:

The new satisfies operator lets us validate that the type of an expression matches some type, without changing the resulting type of that expression.

Using the satisfies operator changes the resulting type of the expression (sometimes). The documentation is false, or the behavior is a bug.

That said, I filed this issue which has been kept open as a separate problem / solution, so maybe one of these ideas will prevail.

Personally, I don't like that using satisfies changes the type outcome (and does so inconsistently), contrary to the documentation, so I dunno, maybe as const is better and satisfies should be fixed. 🤷‍♂️

@sebastian-fredriksson-bernholtz

This comment was marked as off-topic.

@matthew-dean

This comment was marked as off-topic.

@osdiab
Copy link

osdiab commented Jun 5, 2023

This is all stimulating and nice but seems pretty unrelated to the original issue and you’re buzzing subscribers emails without actually indicating progress towards the original goal. Maybe better to have that discussion in a separate GitHub issue/discussion if the above is very important to you?

@mmkal
Copy link

mmkal commented Jun 6, 2023

Would this be easier to implement if it didn't require syntax? What if you could just set this for all json modules in tsconfig:

{
  "compilerOptions": {
    "resolveJsonModule": "const"
  }
}

i.e. change the type of resolveJsonModule from boolean to boolean | 'const'.

I'd be happy enough with that - in the rare case that resolving as const is problematic, it's much easier to go from high- to low-information than the other way round.

@roninjin10
Copy link

Resolving all jsonMOdules as const is dangerous for performance IMO. It's easy for one engineer to turn that on while another one adds an extremely large json array that requires type checking linearly everytime it's used

@matthew-dean
Copy link

matthew-dean commented Jun 6, 2023

@roninjin10 I don't see how. The JSON file would still be inferring X type based on Y value. So the actual process involved would be the same. In fact, one could argue that it should take less time because with some values, it can directly assign the type without widening, but that's entirely dependant on TS internals.

Related: has the argument been made in this thread that as const should maybe be the default type for JSON, such that as const shouldn't be needed? I assume so, but maybe I missed it. I'm wondering what the downside would be for importing as a constant value. If you think about it, file contents data should already be immutable. I guess there would be a rare risk of breaking code somewhere though.

@Peeja
Copy link
Contributor

Peeja commented Jun 6, 2023

@matthew-dean Yes, to infer the type, but once it's inferred, if it's a gigantic union of string literals instead of string (I believe) it would be expensive to throw around the code after it's been inferred.

OTOH, if it's not expensive or otherwise concerning, it seems to me like it would be the ideal default.

@slorber
Copy link
Author

slorber commented Sep 6, 2023

Since ES Import Attributes (Stage 3) are going to be worked on for TypeScript 5.3, I was wondering if it's not a viable option to import JSON as const?

import data from "./data.json" with { type: "json-const" };

I'm not sure about what to use for the type though.

Edit: agree with @matthew-dean here, {type: "json", const: true} looks better.

@matthew-dean
Copy link

@slorber I think altering the type shouldn't be allowed and is counter-intuitive. However, because this is a plain object, I could see TypeScript doing something like this:

import data from "./data.json" with { type: "json", const: true };

That way, it doesn't "alter" the import statement itself, just the with object, which can already extend the import statement.

@Malix-off
Copy link

Would const assertion after declaration be considered too?

@MatthD
Copy link

MatthD commented Dec 6, 2023

Hello everyone, so ATM what is the right solution (if it exist) to match json import with some literals and have the codebase happy ?

@parzhitsky
Copy link

@MatthD If you must have a JSON file and not do any type casting, then there's no solution currently, you'll have to use whatever typings are given to you. Alternatively, you can define a variable and cast it to the necessary type using satisfies or as const approach, but that means having at least some duplication.

@slorber
Copy link
Author

slorber commented Dec 8, 2023

Not a drop-in workaround for json const, but you can type a json file manually.

@mattpocock has a little article on it: https://www.totaltypescript.com/override-the-type-of-a-json-file

CleanShot 2023-12-08 at 13 57 42@2x

@shamrin
Copy link

shamrin commented Feb 29, 2024

@slorber Unfortunately .d.json.ts definition makes TypeScript ignore the content of JSON file. Now JSON can have a shape that does not match its type and one would never notice.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests