microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

Home Page:https://www.typescriptlang.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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

slorber opened this issue · 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" ]
}
  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

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

Playground link

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

Related: #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.)

commented

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

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
  • 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:

  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"

@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. 🤷‍♂️

@matthew-dean

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

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

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