microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.12k stars 12.5k forks source link

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

Open slorber opened 5 years ago

slorber commented 5 years ago

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:

Links:

This feature has been mentionned:

Hawkbat commented 5 years ago

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 commented 5 years ago

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 commented 5 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

// 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 commented 4 years ago

@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 commented 4 years ago

Also happy to work on this if it progresses!

TheMrZZ commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

@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 commented 4 years ago

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 commented 4 years ago

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:

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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

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 commented 3 years ago

Related: https://github.com/microsoft/TypeScript/issues/40694 Implement Import Assertions (stage 3)

SimonAlling commented 3 years ago

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 commented 3 years ago

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 commented 3 years ago

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

ImRodry commented 3 years ago

Hope the issue above gives some more information that could help implement the feature suggested in this one.

parzhitsky commented 3 years ago

There is a stage-3 ECMAScript proposal called "JSON modules", derived from "import assertions", yet another stage-3 proposal:

import json from "./foo.json" assert { type: "json" };

I know that "type" here means something completely different than what it means in TypeScript, and also we might not need to actually assert anything (just infer typings), – but this still feels like something related, maybe we should piggyback on this one.

askirmas commented 3 years ago

Meanwhile

json-d-ts.sh

#!/bin/bash
PATTERN="*.schema.*json"
DECL_POSTFIX=".d.ts"

find . -type f -iname "$PATTERN$DECL_POSTFIX" -exec rm {} \;
JSONS=($(find . -type f -iname "$PATTERN"))

for file in "${JSONS[@]}"
do
  git check-ignore --quiet "$file" ||\
  printf "/** Generated with \`./json-d-ts.sh\` */\ndeclare const data: $(cat $file)\nexport = data" > "$file.d.ts"
done

package.json

{
  "scripts": {
    "preparation": "./json-d-ts.sh"
  }
}
flixcor commented 3 years ago

I made a bundler plugin (webpack/rollup/vite) that helps with this: https://www.npmjs.com/package/unplugin-json-dts https://github.com/flixcor/unplugin-json-dts/

agrajak commented 2 years ago

for those who use @askirmas code and want to escape \n string in JSON, use this one

#!/bin/bash
# https://github.com/microsoft/TypeScript/issues/32063#issuecomment-916071942
PATTERN="*.schema.*json"
DECL_POSTFIX=".d.ts"

find . -type f -iname "$PATTERN$DECL_POSTFIX" -exec rm {} \;
JSONS=($(find . -type f -iname "$PATTERN"))

for file in "${JSONS[@]}"
do
  text=$(cat $file | jq -sR .)
  length=${#text}-2
  git check-ignore --quiet "$file" ||\
  printf "/** Generated with \`./json-d-ts.sh\` */\n/* eslint-disable */\ndeclare const data: $(echo ${text:1:$length})\nexport = data" > "$file.d.ts"
done
Swazimodo commented 2 years ago

What needs to be done to move this forward? From the thread it looks like import myJson from './myJson.json' as const is the agreed upon syntax.

thw0rted commented 2 years ago

I don't see any activity from the TS team, which is surprising given that it has nearly 300 thumbs-up reactions. @RyanCavanaugh marked it "awaiting feedback" but there appears to be plenty of that. I think all the community can do now is wait?

albnnc commented 2 years ago

Just casually found this issue in a process of understanding that const-typed json imports are currently impossible in TS. I also think that this feature would be incredibly useful for DX, which is the primary target for TS as far as I can understand. Would like to see this implemented.

lukeapage commented 2 years ago

300 isn’t that many - sort open issues by thumbs up and this comes on the 2nd page. A lot that are ahead of this really surprise me. More thumbs up might help, the teas has said earlier they look through issues sorted by it periodically.

willmartian commented 2 years ago

Suppose I wanted to import a JSON for the sole purpose of using it as a const type. This syntax hasn't been suggested yet, but it feels consistent to me:

import type MyElement from "custom-element-manifest.json"

(As that code snippet alludes to, a cool use case for this would be mapping types from things like the custom elements manfiest.)

btoo commented 2 years ago

maybe import type can be used for both normal and const json imports:


import type SomeJsonType from "some-json.json"

is equivalent to

import someJson from "some-json.json"
type SomeJsonType = typeof someJson

import type SomeConstJsonType from "some-json.json" as const

is equivalent to

import someConstJson from "some-json.json" as const
type SomeConstJsonType = typeof someConstJson
Platane commented 2 years ago

The import type syntax is fully erased by typescript. Which ensure that the module is not require at runtime.

Whereas with

import someConstJson from "some-json.json" as const
type SomeConstJsonType = typeof someConstJson

You might end up with the content of some-json.json in your bundle ( depending on how good your bundler is at treeshaking ).

It's not strictly equivalent.

somebody1234 commented 2 years ago

Since there's been no maintainer activity I might as well give a shot at implementing this. The parser changes seem trivial enough, the typechecker changes are a little harder. For context here's the code determining whether an expression is as const:

        function isConstContext(node: Expression): boolean {
            const parent = node.parent;
            return isAssertionExpression(parent) && isConstTypeReference(parent.type) ||
                isJSDocTypeAssertion(parent) && isConstTypeReference(getJSDocTypeAssertionType(parent)) ||
                (isParenthesizedExpression(parent) || isArrayLiteralExpression(parent) || isSpreadElement(parent)) && isConstContext(parent) ||
                (isPropertyAssignment(parent) || isShorthandPropertyAssignment(parent) || isTemplateSpan(parent)) && isConstContext(parent.parent);
        }

As you can see, it's determined completely by parent nodes... which makes sense since the only source of as const is, well... as const. So the main problem with this change is having to associate multiple types to a single node. (Another problem would be figuring out how to propagate "I want the const type of this" through the module resolution + typechecking - and how to cache the types, how to avoid computing the const type if it's not needed etc. Also worth noting this would only apply to JSON modules so a complete restructure of how checker.ts works should be avoided if at all possible) (If anyone has any ideas, please feel free to comment)

bombillazo commented 2 years ago

It would be phenomenal to have this feature!

JohnyTheCarrot commented 2 years ago

I need this as well. Would be great!

lobotomoe commented 2 years ago

"Awaiting More Feedback"? Srsly? :)

ShivamJoker commented 2 years ago

I also stumbled upon this today. Why don't we have this yet?

slifty commented 2 years ago

I made the same mistake as someone else and commented in #26552 but should post here! Hopefully this isn't adding to noise...


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"'
thw0rted commented 2 years ago

@slifty you might be interested in https://github.com/vega/ts-json-schema-generator -- I had a similar problem, and used that to go from TS interface(s) -> JSON schema, rather than the other way around.

lbguilherme commented 2 years ago

This would be really useful for the as-typed package, as it would allow to import and use types from JSON schemas directly.

import type { AsTyped } from "as-typed";
import type schema from "./schema-data.json" as const; // <--

export type Business = AsTyped<typeof schema["components"]["schemas"]["Business"]>;

Currently the only way to use it is to copy the JSON file into a TypeScript file with as const added.

varanauskas commented 2 years ago

On the same note as @lbguilherme this would also be useful for a library I am creating by allowing users to import OpenAPI spec straight from JSON without having to do the

declare const data: {};
default export data;

song and dance

Ustice commented 1 year ago

I have had two uses for this on my current big project. The first is loading static data. I would much rather just define it, and infer the types from there. It really stinks to have to do that in two places.

The second is that we are making a utility to manage environment variables. I would really like to have all of the used environment variables in a json file that I can load, and turn into a tuple of static values. When someone references a new one in the code, it will update the json file with the new key. I can totally do this by creating a code generator, but it would be really nice if I could just statically import the data as const.

adlerfaulkner commented 1 year ago

Would also be extremely useful in Standard SDK, which dynamically reads OpenAPI specs from JSON to automagically create an SDK for any REST api.

I'm getting:

Type 'string' is not assignable to type '"path" | "header" | "query" | "cookie"'

when an imported OpenAPI spec is used as an argument which is typed with an OpenAPI validation type to a function that creates the SDK. I want to use the value of this argument (the OpenAPI spec) to add Typescript autocompletion to the SDK it creates. It is critical to our project to allow users to import OpenAPI specs "as is" and not have to got through the process of converting them to javascript objects first.

KholdStare commented 1 year ago

@RyanCavanaugh Just another friendly poke :) It has been more than 3 years (!!!) since this feature was requested. There are quite a few projects that would greatly benefit from this.

DetachHead commented 1 year ago

i thought #51865 would've allowed for a workaround for this, but i guess not

// foo.json
{
    "foo": "bar"
}

declare const narrow: <const T>(value: T) => T
const narrowed = narrow((await import('./foo.json')).default) // not narrowed. type is still {foo: string}
somebody1234 commented 1 year ago

yeah unfortunately not, because import is the thing resolving the type, it's not a literal that can be narrowed

ScreamZ commented 1 year ago

Is there any way to achieve this for now? I'm trying to introspect a model.json file from prismic to generate some custom types.

TSMMark commented 1 year ago

In case this helps anyone, I'm using babel plugin codegen to generate types by reading the json files from disk. Very cumbersome, but better than writing types manually.

j-murata commented 1 year ago

Is there any chance that the const type Parameters in TS 5.0 will solve this problem?