import ConstJson from './config.json' as const;
slorber opened this issue Β· 83 comments
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:
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:
#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?
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" ]
}
- Sealed tuplet:
readonly [ "FR", "BE" ]
- Tuplet:
[ "FR", "BE" ]
- Array of custom strings:
("FR" | "BE")[]
- 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
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:
- 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.
@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.
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?
@j-murata nope. see these two comments above #32063 (comment) #32063 (comment)
@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 ?
Four years have passed.
Would be extremely useful. Without this we need to write a cli tool that generates a .ts file from a json so I can as const it.
Just to comment on this thread, I think importing as const
for a JSON is a bit of the wrong direction, since that doesn't really successfully coerce all the types that you might expect into the shape you actually want.
A surprising behavior of satisfies
is that it does type coercion, and not just type-checking.
For instance, you may not realize it (or at least I didn't), but these two objects will have two different types:
let foo = {
variant: 'primary'
}
let bar = {
variant: 'primary'
} satisfies { variant: 'primary' }
The first object's variant
key has a string type. The second object's variant
key has a string literal type where primary
is the only valid value.
Meaning that following the above with this will cause a TypeScript error:
bar = foo
So, rather than
import myJson from './myJson.json' as const
I think the syntax that actually has the flexibility to solve different problems in this thread would be more like:
import myJson from './myJson.json' with { satisfies: SomeType }
That would / could coerce a type into string or number literals exactly where you want it, and preserve the existing type inference where you didn't.
OR: Alternatively: it would be great if these two things had the same behavior, and would would require no additional syntax (like with the with
keyword):
const value = {
variant: 'primary'
} satisfies SomeType
import value from 'value.json'
value satisfies SomeType
Sidenote: Please stop noting the linear passage of time. Language features are not handled "first in first out".
tl;dr: "please stop noting the linear passage of time" is the opposite of constructive feedback.
however, they are expected to be responded to in a timely fashion, rather than having no official feedback whatsoever. and it's not even like it'd be that difficult to constructively give feedback:
- this is not being considered for <x> reason
- this is more trouble than it's worth because <y> makes it hard
- we are accepting PRs, <this> is how it should be done if anyone's interested
- we are still awaiting feedback, <z> is still unclear
- there are issues with it, for example <a>, <b> and <c>
i personally think it's a little silly to address the two comments that note the passage of time and completely ignore the other 95%.
it's also worth noting that saying "please don't note the linear passage of time" will not stop people from saying that
There are thousands of open suggestions, and I am doing the best I can to leave comments explaining why certain features haven't made the iteration plans, as well as explicitly rejecting certain features that have been opened for a long time. You can see this in places like minification, regex types, typed exceptions, etc.. I'm doing the best I can and I'm asking people to just act in a way that makes it easier for me to do that, which I think is aligned with everyone's interests since posting the comments about how long something has been open doesn't help anyone.
Part of the difficulty in writing those comments is honestly the sheer volume of nonspecific feedback ("Any updates?" "This is has been open for N years") that doesn't contribute meaningfully to the discussion, and leads people to make duplicative comments because things that have already been said are hiding behind "Load [n] more comments" blocks.
tl;dr: i don't think there's a "sheer volume" of nonspecific feedback here - at the very least it doesn't make things "difficult" in this thread specifically. as i have already pointed out there are only two topics about the amount of time passed. there are (currently) 40 hidden items. even completely removing said comments would reduce the amount of messages behind "load [n] more comments" blocks by 5% which is an insignificant fraction.
tl;dr 2: as mentioned below, this is the tenth most upvoted suggestion, hence i believe it deserves at least two minutes spent writing constructive feedback. this averages out to 30 seconds per year this issue has been open which i do not think is an unreasonable amount of time to spend.
sure, that's fair. regardless:
- i'm still not convinced that your first comment is constructive.
- yes there are thousands of open suggestions but hear me out: this is the tenth most upvoted suggestion.
- please also note that i believe this is is not:
- a major undertaking that creates additional complexity for the end-user, as with regex types
- not feasible/not that useful without major DT changes, as with typed exceptions
- out of scope (or already handled by dedicated build tools for years), as with minification
- please also note that i believe this is is not:
- as i've already pointed out, said non-constructive comments are a vanishingly small fraction of this thread. i think it'd be much faster, less controversial, and just better in general for everyone involved to just silently mark them as off-topic instead of intentionally bringing attention to them.
@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:
- It's better to discuss this issue in terms of 'type narrowing" and "type widening" rather than the much broader "type coercion".
satisfies
does not technically do any type coercion - or type narrowing/widening for that matter. The entire purpose of thesatisfies
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?
- 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 typestring
, but can be interpreted as both the TypeScript primitive typestring
and the string literal type"hello"
. - 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.
- 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"
@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. π€·ββοΈ
There is still no "type coercion", only type inference. And while my explanation isn't entirely correct for your example, it becomes so with a slight change of wording from
can only interpret it as the literal type
to
is "better" to interpret it as the literal type
So, what is going on in your example?
TypeScript is doing it's best trying to infer what type foo
should be from the information you're providing and being as unobtrusive as possible. Since foo
is a let
- as opposed to const
- it can infer that you intend foo
to change. Hence, it's "better" to interpret 'one'
as string
than as 'one'
.
I do think it's bug though and worth an issue to request that foo
is inferred to be 'one' | 'two'
rather than string
in your case, but it definitely shouldn't be inferred as 'one'
.
The documentation could be clearer, but if you think of literal values as not having a specific type, the documentation isn't incorrect. satisfies
does not change the type of the expression, the expression is not of a (specific) type. satisfies
simply provides context for how to infer the type when the type is yet to be determined.
As you can see from this simple example satisfies
has no impact on the inferred type if the value "is of a specific type":
let foo = 'one' as 'one' satisfies 'one' | 'two'
// foo is type 'one' because 'one' is of type 'one'
I'm still interested in what you think is the problem with as const
assertion?
satisfies does not change the type of the expression, the expression is not of a (specific) type. satisfies simply provides context for how to infer the type when the type is yet to be determined.
To me, that's a distinction without a difference. "without changing the resulting type of that expression" is the key phrase in the documentation e.g. the resulting type should be identical whether satisfies
is present or not. Calling it "inference" vs coercion is also just semantics in this instance. It would have been one type, but it is not, because of the satisfies
keyword.
To be clear, I'm fine if satisfies
has this behavior if it is consistent and if it's noted in the documentation that satisfies
does have an influence on the "resulting type". That's fair, isn't it?
As to this:
I'm still interested in what you think is the problem with as const assertion?
There were a few comments in this thread about how exactly as const
should behave. I think it really depends on how TypeScript restricts how the inferred type can be used.
I guess if it imports JSON as the narrowest type possible, that's fine (if that means it can be passed to functions or assigned to variables expecting broader types), but even in testing as const
in the TypeScript playground, it doesn't infer types in the way I expect.
Take this for example:
const foo = [
{
variant: 'primary',
num: 1
},
{
variant: 'secondary',
num: 2
}
] as const
This results in this type:
const foo: readonly [{
readonly variant: "primary";
readonly num: 1;
}, {
readonly variant: "secondary";
readonly num: 2;
}]
But what I really wanted was:
type Foo = Array<{
variant: 'primary' | 'secondary'
num: number
}>
or even
type Foo = Array<{
variant: 'primary' | 'secondary'
num: 1 | 2
}>
There's no real way to tell as const
what to do. Within the code, as we've been discussing, the only way to coerce or "help infer", whatever you want to call it, along with type-check the object is with satisfies
.
If I do:
const foo = [
{
variant: 'primary',
num: 1
},
{
variant: 'secondary',
num: 2
}
] satisfies Array<{ variant: 'primary' | 'secondary', num: number }>
.....well, actually we still don't quite get there, because it still doesn't infer the type that we explicitly gave it, even though it correctly satisfies it. π (Honestly, the more I experiment with satisfies
in relation to this and related issues, the more I'm confused with how it works or is supposed to work.)
We get a type that is:
const foo: ({
variant: "primary";
num: number;
} | {
variant: "secondary";
num: number;
})[]
...which is clunky and not what I meant for it to infer, but okay. (And doesn't infer the type similarly to the non-array example, so I'm just more confused.) But still, the point is that I can nudge the TypeScript inference using satisfies
in a way I cannot using as const
. And, more importantly, I can type-check using satisfies
while I can't with as const
.
I think though, to be fair, using as const
might be "good enough" for most scenarios? And maybe could be further inferred or coerced using some kind of advanced type construct. I'm not sure I would put as const
on the import
syntax, instead of extending the new with
syntax for imports, but maybe it's fine? π€·ββοΈ
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?
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.
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
@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.
@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.
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.
@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.
Would const assertion after declaration be considered too?
Hello everyone, so ATM what is the right solution (if it exist) to match json import with some literals and have the codebase happy ?
@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.
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
@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.
Hi I found this issue since I think it would be a great addition to infer Types from Json Schemas! See ThomasAribart/json-schema-to-ts#200
Happy to help if someone can point me in the right direction.
as a workaround, should we implement a simple converter json2js, it would take a my.json file an create a my.js file.
//my.json
{
"hello": "string"
}
//my.js
export const my = // <- add this line
{
"hello": "string"
}
all it needs to do is to add the first line
and then we can
import { my } from 'my.js'
What do you do when the json filename is an invalid symbol name, i.e. it has hyphens?
@couhajjou-apps @GabenGar
No need to reinvent it:
- As "regular" interface: https://quicktype.io/typescript
- As const interface (what this issue is about): https://www.npmjs.com/package/ts-json-as-const
@couhajjou-apps If we're going that route, I'd rather use default export rather than the named one to avoid dependending on the arbitrary filename:
- {
+ export default {
"foo": 42,
"bar": 17
}
@couhajjou-apps @GabenGar No need to reinvent it:
- As "regular" interface: https://quicktype.io/typescript
- As const interface (what this issue is about): https://www.npmjs.com/package/ts-json-as-const
I don't see how it's related and why it has 3 thumbs up. Weird.
@couhajjou-apps If we're going that route, I'd rather use default export rather than the named one to avoid dependending on the arbitrary filename:
- {
- export default {
"foo": 42,
"bar": 17
}
Good idea !
What do you do when the json filename is an invalid symbol name, i.e. it has hyphens?
I'm unsure of what you're asking.
Oh! The symbol never needs to be the same as the imported module's filename. It can be arbitrary e.g.
Import hyphenJson from './-.json' with { type: 'json' }
This discussion isn't about importing JSON modules nor is it about importing them with an attribute so it's imported only with a JSON parser (that was resolved by https://github.com/tc39/proposal-json-modules).
This discussion is about getting the type information from the imported object as if it was an object defined with the TypeScript syntax as const
, though what they want differe from how as const
actually works.
const arr0 = [
"entry0",
1,
];
// typeof arr === (string | number)[]
const arr1 = [
"entry0",
1,
] as const;
// typeof arr1 === [ "entry0", 1 ]
If they want ["entry0" | 1]
or something similar, they'll have to use type manipulation like DeepWritable<T>
from the ts-essentials package.
The exact thing that as const
does, works extremely well for the config use case- as long as you invert the relationship to assert that the JSON satisfies
the type of the places where it is used. However, in 2024, there's a lot more tooling around that makes it much easier to do runtime validation of ts types, which ultimately is the more correct approach anyway.
There are still some annoying type narrowing / widening problems that can arise, but satisfies
can solve many of them without overly verbose definitions.