Open slorber opened 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' | ...
.
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 😁
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
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.
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.
// 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.
@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:
const foo = 'abc'
. I think this would at first pass look more like a variable assignment than an importThoughts? Have I missed any alternative syntax options?
Also happy to work on this if it progresses!
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.
As great as this suggestion is, how should TypeScript interpret the type of property in the original comment?
{
"appLocales": [ "FR", "BE" ]
}
readonly [ "FR", "BE" ]
[ "FR", "BE" ]
("FR" | "BE")[]
string[]
← the current oneI 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
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.
@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).
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?
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.
@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 ?
@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!
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
This currently requires as const
on the translations object, which we can't do because it lives in a json file.
Related: https://github.com/microsoft/TypeScript/issues/40694 Implement Import Assertions (stage 3)
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).
/>
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.
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
Hope the issue above gives some more information that could help implement the feature suggested in this one.
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.
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"
}
}
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/
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
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.
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?
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.
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.
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.)
maybe import type
can be used for both normal and const
json import
s:
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
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.
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)
It would be phenomenal to have this feature!
I need this as well. Would be great!
"Awaiting More Feedback"? Srsly? :)
I also stumbled upon this today. Why don't we have this yet?
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"'
@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.
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.
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
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
.
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.
@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.
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}
yeah unfortunately not, because import
is the thing resolving the type, it's not a literal that can be narrowed
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.
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.
Is there any chance that the const
type Parameters in TS 5.0 will solve this problem?
Search Terms
json const assertion import
Suggestion
The ability to get const types from a json configuration file.
IE if the json is:
I want to import the json and get the type
{appLocales: "FR" | "BE"}
instead ofstring
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: