Open slorber opened 5 years ago
@j-murata nope. see these two comments above https://github.com/microsoft/TypeScript/issues/32063#issuecomment-1409510267 https://github.com/microsoft/TypeScript/issues/32063#issuecomment-1409556488
@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:
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:
@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:
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?
"hello"
is of runtime type string
, but can be interpreted as both the TypeScript primitive type string
and the string literal type "hello"
.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
@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 https://github.com/ThomasAribart/json-schema-to-ts/issues/200
Happy to help if someone can point me in the right direction.
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: