Repository navigation
satisfies should work with imported / referenced values #54488
Description
Suggestion
Right now, satisfies works on values and object literals right after they're declared. However, there are use cases when satisfies can't be used immediately, and the value is imported, such as JSON files.
🔍 Search Terms
JSON, typed JSON, satisfies JSON
✅ Viability 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, new syntax sugar for JS, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
⭐ Suggestion
In short, I'd like to be able to do this:
import book from 'book.json'
const foo = book satisfies BookResourceor, alternatively:
import book from 'book.json' with { satisfies: BookResource }and have it work equal to:
const book = {
// book fields
} satisifies BookResource📃 Motivating Example
I have this structure in my book data JSON file:
{
"data": [
{
"type": "book",
"baseCode": "394b76ce-3ad9-4c83-b21d-d5eab7962249",
"id": "394b76ce-3ad9-4c83-b21d-d5eab7962249",
"url": "http://github.com/donec/dapibus/duis/at.jpg",
"originalUrl": "http://qq.com/ante/ipsum/primis.json",
"imageUrl": "http://dummyimage.com/246x100.png/5fa2dd/ffffff",
"retailer": "Amazon",
"title": "Dead End Drive-In",
"author": "Vlad Cutmore"
},
{
// more of the same schema
}
]
}And I have a type (interface) like:
interface BookResource {
id?: string
type: 'book'
baseCode: string
imageUrl: string
title: string
author: string
originalUrl: string
retailer: string
url: string
}(Note, in this scenario, the source data MUST be JSON.)
I want to make sure that if anyone modifies the JSON, they get a type error. However, doing the following results in an error:
import books from 'books.json'
// ...
const bookData = books.data satisfies BookResource[] // error - "Type {...} is not assignable to BookResource"Now, I wasn't sure if this should be marked as a bug or feature request, because this works:
const blah = {
type: 'book',
baseCode: '394b76ce-3ad9-4c83-b21d-d5eab7962249',
id: '394b76ce-3ad9-4c83-b21d-d5eab7962249',
url: 'http://github.com/donec/dapibus/duis/at.jpg',
originalUrl: 'http://qq.com/ante/ipsum/primis.json',
imageUrl: 'http://dummyimage.com/246x100.png/5fa2dd/ffffff',
retailer: 'Amazon',
title: 'Dead End Drive-In',
author: 'Vlad Cutmore'
} satisfies BookResourceIn other words, the exact same value data, when applied with the satisfies keyword, causes an error depending on where the value is being checked, even though it's the same value.
What seems to be the case is that satisfies gently coerces the type (specifically to "book" instead of string) in the latter example, but fails to do so when the value is imported.
💻 Use Cases
Currently, I want to use this for JSON, but note that there are other cases where you might want to "check" and gently nudge the type at compile time the exact same way that happens with satisfies at assignment time.
Because, similar to the above example, this also causes an error:
const foo = {
type: 'book',
baseCode: '394b76ce-3ad9-4c83-b21d-d5eab7962249',
id: '394b76ce-3ad9-4c83-b21d-d5eab7962249',
url: 'http://github.com/donec/dapibus/duis/at.jpg',
originalUrl: 'http://qq.com/ante/ipsum/primis.json',
imageUrl: 'http://dummyimage.com/246x100.png/5fa2dd/ffffff',
retailer: 'Amazon',
title: 'Dead End Drive-In',
author: 'Vlad Cutmore'
}
const bar = foo satisfies BookResourceIn TypeScript / JavaScript terms, these should be equivalent statements, and TypeScript should be able to
- say that the value of
foosatisfies the type ofBookResource - coerce
barto be oftype: "book"vstype: string, just as it would ifsatisfiesimmediately followed the value.
1st note: obviously I could just use as BookResource instead of satisfies BookResource, but of course that bypasses the type-checker entirely, and defeats the purpose. Then, the JSON could have any data / shape whatsoever, with no errors provided.
2nd note: I can also sorta get there with satisfies Array<Omit<BookResource, 'type'> & { type: string }> but that's kind of gross, and I want to enforce that the type truly is "book" and not just a string. JSON type inference is helpful but it isn't perfect, and currently there's no way to actual tell it how to interpret string literal types.
Activity
RyanCavanaugh commentedon Jun 1, 2023
It seems like this does what you want? What falls short here?
I don't think we want to mix-and-match TS and ES syntax in
importany more than we have to, especially when the equivalent desugared code is extremely similar.jcalz commentedon Jun 1, 2023
I think this is probably morally a duplicate of #32063 (to import a json module "
as const")(Ryan Cavanaugh (@RyanCavanaugh) the
typeproperty needs to be the string literal type"book"in the example, but that won't happen)matthew-dean commentedon Jun 1, 2023
Ryan Cavanaugh (@RyanCavanaugh) Here is a reduced test case: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgEIHt0GsAqBPABxQG8AoZZMQiALmQHIAjTLe0gX1NIXRAGcwyGJmQBeZGQpUidJizadSw9Mj5wwwPjGAQ+aFviJcgA
X satisfies Yas a statement would be great if it worked, but it doesn't.matthew-dean commentedon Jun 1, 2023
Joe Calzaretta (@jcalz)
It's partly the same core problem / bug / issue, I guess you could say, which is that TypeScript's type inference of object shapes is often useful and also, often wrong, and there's no real option to coerce the types, except to completely override the type into any possible invalid interpretation of the type, which doesn't solve anything.
The surprising discovery that I don't think was covered in the documentation write-ups on
satisfiesis that it actually changes / coerces the type, instead of being simply a type of type check assertion. Since that is part of the behavior ofsatisfies, then IMO it makes more sense to be able to coerce (and check) the type usingsatisfieson any value, rather than importingas constbecause that's incredibly limited.So IMO the linked issue is talking about a very narrow part of the problem, and discussing a solution that won't actually address that problem fully.
RyanCavanaugh commentedon Jun 1, 2023
By "wrong" here do you just mean "is less specific than I would have like to be inferred" ? Or is there an actual "it says it's a string but it's actually a number?" instance?
matthew-dean commentedon Jun 1, 2023
Ryan Cavanaugh (@RyanCavanaugh) Well, what I mean is more that the
satisfiesstatement:satisfiestype in some circumstances and rejects it as a mis-matched type in othersThe fact that
satisfiescoerces the type at all was counter-intuitive to me, because I thought it was just a syntax for validating the type. But because it has that behavior, it would be useful for coercing the type of a JSON file to the expected type.RyanCavanaugh commentedon Jun 1, 2023
I mean, sure, but you wouldn't want it to not do that, because then a bunch of assertions that you would want to succeed would fail.
matthew-dean commentedon Jun 1, 2023
Ryan Cavanaugh (@RyanCavanaugh) Correct. I'm not suggesting changing
satisfiesexisting behavior. I'm suggesting keeping this coercion consistent with any other value, referenced or imported, so extendingsatisfiesto any value, not just a value at the original definition time.matthew-dean commentedon Jun 1, 2023
i.e. I would be overjoyed if this worked:
If that's all it took for
bookto be coerced to BookResource, just as it would be inline, that would be wonderful.matthew-dean commentedon Jun 1, 2023
Ryan Cavanaugh (@RyanCavanaugh) Joe Calzaretta (@jcalz) We can close this if #32063 tracks this better. I just didn't want to completely derail if people are attached to the idea of
as const(although I would argue that wouldn't completely solve the problem).RyanCavanaugh commentedon Jun 1, 2023
I think these are separable and both valuable. The scenario of a middle ground between the entire blob being
const, which is often way too specific, and just getting the inferred type, which might not be as specific as you want, makes sense.jcalz commentedon Jun 2, 2023
If we had
as constthen you could import the json that way and widen to whatever type you wanted in a different variable. (Well, you would, ifreadonlyarrays were actually subtypes of mutable arrays and not the other way around.) That might be less ergonomic thansatisfiesfor this use case, but it would at least be possible. Right now we only have the inferred type, or something you just claim is the type from a.json.d.tsfile as mentioned in #49703 (comment)Oh, that reminds me that this could also be "morally equivalent" to #52994 (which is clunkier than this suggestion, imo).
I don't know that any of these should be closed as duplicates of each other, but I think they are all circling around the same unmet need to allow something like a type annotation for imported json. I'd hate for those who want that to split their vote among particular flavors of the feature, if it lowered the likelihood of any of them being implemented.
matthew-dean commentedon Jun 3, 2023
In this case, JSON is the primary use case, yes, but there are actually a number of related TypeScript issues of wanting to narrow the type within a block scope without sending it through an assertion function, or when a type inferred through an object value satisfies two types but TypeScript insists it doesn't, because it's already set the object's inferred type in stone.
A possible solution, then, might also be simply that TypeScript:
satisfies, and/or#2is probably a little too complex and might have unexpected consequences, but it violates the Law of Least Surprise at least a little bit, when TypeScript says that an object with a string literal value cannot be assigned to a variable or passed to a function that's expecting the type of an object with that very same string literal. i.e. the concrete value can match the expected type perfectly, which should be passable, except that the inferred type does not match. So the problem is both the nature of inference, and the nature of type checks.matthew-dean commentedon Jun 3, 2023
Really, this issue is more fundamental than objects or JSON, as demonstrated by this playground example, in which the behavior of
satisfiesis (IMO) highly inconsistent (regarding whether or not it coerces types). But also the variables offirstandsecondshould be passable, because their values are statically analyzable (otherwise a type ofstringwouldn't have been inferred in the first place).So, in addition to this issue, which is like a feature request, I would suggest there's also a
satisfiesbug, in which it sometimes coerces a type, and sometimes doesn't.Also, can I point out that the TypeScript documentation has this to say:
That was how I understood
satisfies, but that statement is demonstrably false. It does change the resulting type of the expression (but only sometimes!), as demonstrated in multiple examples here. So I'm not sure if the behavior ofsatisfiesshould be expanded (to all values) OR it should be "corrected" (to not coerce types) OR if the documentation should be changed. I'm really not sure whatsatisfiesis supposed to do, because whatever it does do, it does inconsistently.jcalz commentedon Jun 3, 2023
#47920 is a big discussion but it explains that one intended use of
satisfiesis to provide contextual typing, so it definitely can affect "the resulting type of the expression". The sentence you object to in the handbook unfortunately ignores this; I presume the point there was to distinguish it from type annotations, which almost always end up discarding more specific information from the initializer.But at no point was
satisfiesintended to retroactively change the type of some existing variable. Contextual typing doesn't propagate backwards indefinitely. Sois the intended use case, and is fundamentally different from
where instead of providing context for the value
[1, 2], we're apparently trying to do so for the variablex, which is already "set in stone" as you say. As far as I know there's no mechanism anywhere in TypeScript that works like that. You can't re-annotate after the fact; you can'tas constafter the fact; you can'tsatisfiesafter the fact: it's just too late. I don't imagine this will change (but I've been wrong before so 🤷♂️; it would be nice to hear some official word).So I could see any of
being implemented since you're declaring the variable in the same statement with your desired type, but
doesn't look feasible.
matthew-dean commentedon Jun 4, 2023
Joe Calzaretta (@jcalz)
Thanks for the additional info. I do hope the documentation will be reworded.
One thing, this:
Results in a type of x of
[1, 2], withletorconst, BUT...results in a type of
number.I wonder now if this was intentional? Maybe trying to guess developer intent with satisfies? I mean, to me, the developer wants to make sure
xstays within1 | 2, but maybe the TS team assumed then they would write:I dunno, still trying to wrap my head around the intended behavior.
sebastian-fredriksson-bernholtz commentedon Jun 5, 2023
Matthew Dean (@matthew-dean)
as constassertion still seems to fulfil all of your requirements so far, given the slight inconvenience of dealing with readonly array that @jcalz mentioned.In response to your examples in #32063 (comment), if all you want to do is to validate that your JSON conforms to a certain type, you can just use
satisfiesoperator on theas constimport.If the resulting type is too strict for you, i.e. you don't only want to validate the JSON, but you also want to make changes to it according to your defined type, you can easily achieve that as well:
Notice my usage of type annotations over the
satisfiesoperator. Type inference, type annotations,satisfiesoperator, andas constassertion all have different - well defined - purposes. If you want a variable to be of a certain type, that is what type annotations are for. Type inference is just that, "inference". TypeScript cannot read your mind about what type you think a variable should be. It simply makes a best guess, given the context, as a convenience so that you don't have to annotate everything manually.The purpose of the
satisfiesoperator - as I understand and would possibly suggest to reword it - is toIn the general case (where the expression is of a type) that statement is equivalent to the existing one, because "changing" a type can only mean
Given the implicit understanding that it won't change the resulting type to one that does not match the comparison type, and that it won't arbitrarily narrow the type of the expression further (the type of the expression must already be "narrower" to match the comparison type), the only thing that "changing" could mean is "widening". Hence the statements are equivalent.
The special cases where the statements are not equivalent are when the expression doesn't actually "have a type". At this point I think it's worth pointing out that TypeScript works on types, not values. This seems to be where much of your confusion stems from.
As mentioned, while literal values have a runtime type and there are corresponding TypeScript types for those runtime types, the type of literal values (as opposed to TypeScript identifiers) is not really defined (I guess they could also be considered to be the literal type).
Now we are getting closer to explain some of the inconsistencies with the
satisfiesoperator. They are all related to applyingsatisfieson a value. However, since TypeScript only works on types, and thesatisfiesoperator "validates that the type of an expression matches some type", it's not really defined what happens when you apply it to an expression with undefined type. Alternatively, it should leave the type of expression as undefined.Here it's also important to distinguish between the type of the expression, and the type of the variable the expression is assigned to. As shown with
t1above, TypeScript infers the type of the variable when the type of the expression (the value) is not defined (alternatively the literal type). None of the examples has actually shown that the type of the expression has changed, only that the inferred type of the variable you assign the expression depends on thesatisfiesoperator. This does not contradict the statement about howsatisfiesoperates. The type of the expression might still be considered undefined - or the literal type - for all that we have seen.You're not wrong though, there are inconsistencies in how
satisfiesaffects inference when assigning literal values, although from what I've seen so far, they are usually reasonable guesses from the context. I do think it could be improved by never inferring a type that does not itself match the comparison type, i.e. it should widen the type to the widest type that matches all comparison types in the expression.There are also cases where
satisfiesinarguably changes the type of the expression, specifically for object literalsAll of the code has been tested in TypeScript version 5.0.4 (VSCode) and 5.1.3 (TS Playground).
RyanCavanaugh commentedon Jun 5, 2023
Have folks read the very long discussion about this behavior in the original
satisfiesthread? #47920matthew-dean commentedon Jun 5, 2023
Ryan Cavanaugh (@RyanCavanaugh) Ah I see. A number of people objected against contextual typing for reasons of confusion that I encountered, and you argued for
satisfies T as Tas a safe way to cast to exactly what you want ifsatisfiesdoesn't contextually type in a way you expect.satisfies T as Tseems so insanely useful that I wish there was simplified syntactic sugar for it. It probably at least deserves it's own section in the documentation, because it solves a number of things immediately but it's not immediately intuitive (since people are often thinking of swapping outasand replacing withsatisfies, and not both).matthew-dean commentedon Jun 5, 2023
Sebastian Fredriksson Bernholtz (@sebastian-fredriksson-bernholtz)
Not really. To add more context, let me make it more clear. I refactored some sample data files from
.tsto.json, because they were going to be shared / maintained by both frontend and backend developers.In doing so, I, of course, had to remove the
satisfieskeyword on the dataset, which I was using to make sure the sample data adhered to the defined data contract, and if someone changed something that didn't, OR they changed the data contract type, then we would get a TypeScript error during type check of the PR.So now we have data with no contract.
as constwould simply import the data as-is, but that doesn't help us at all. I could probably (maybe?) do something like define a variable with the data contract type, and assign it to the JSON data, and see if there's a type error, but of course that leaves a pointless runtime assignment. Maybe not a big deal, but I was just hoping for a way to type check (and contextually type) JSON in the same way I was using before.as constwould simply narrow the type, but that still doesn't tell me if the type it narrows to is valid, whilesatisfiesdoes.EDIT: I suppose the other thing I could do is have a "dummy" file that tries to assign or pass the JSON to a narrowly-defined type, which doesn't get bundled into the runtime? It just seems like a clumsy workaround. 🤷♂️
RyanCavanaugh commentedon Jun 5, 2023
Something I was curious about was the nature of these JSON files. It seems like you have some schema for these, obviously, since you have some type to write down. Is what you actually need a thing you can run during your build pipeline to make sure that the JSON file matches the .d.ts, and it would just be convenient if that thing was tsc? Or are there separate things, like the schema is actually a super type of the type that you want to have at compile time and you're using satisfies to narrow it down?
matthew-dean commentedon Jun 5, 2023
Ryan Cavanaugh (@RyanCavanaugh) In our case, if I understand your question correctly, the type in the JSON file should entirely match the defined type, which defines a REST response object. Right now we're manually defining those types in TypeScript, but we do plan to use a tool (ServiceStack) to export those TypeScript types from their C# data contract definition.
So, at minimum, we do want to type-check them at compile time. And its not the worst outcome if TSC is the place that catches those errors. But from a DX point of view, like many devs, most of my interactions with TypeScript / types happen in VSCode, and it would be nice to immediately see an error with a mis-matched type, before running
tsc(or, in our case,vue-tsc). I wasn't sure if you were implying that a.d.tsfile would do that? My perception is that.d.tsis a little like usingas Ton an object (as in, overrides the type of an export?), but maybe that's a mis-perception.sebastian-fredriksson-bernholtz commentedon Jun 5, 2023
Matthew Dean (@matthew-dean)
Maybe I'm missing something but I'm not saying you should remove
satisfiesI'm saying that if you use it withas constyou can use it as you originally wanted.as const satisfies Tis what I'm doing in all of the examples.If you really don't want the assignment (I don't see why that is such a concern - is it gigantic?) you might possibly be able to do:
Or you can do it inline where it's being used
RyanCavanaugh commentedon Jun 5, 2023
I guess I'm thinking about a bunch of different scenarios
Scenario 1: You auto-generate, or hand-edit, a single JSON file, and want to ensure that its type matches a predefined contract. You don't want to use
as constbecause the actual implied domain of the types isn't uniformly literals, or because you might want some discriminated unions in there that can't be inferred from the shape.In this scenario, you don't need
tscto be doing the checking (you can write a tool to do this out-of-band), but it'd be niceScenario 2: You have a variety of JSON files that might match some variety of top-level schema, e.g. you might have a few different config file shapes and use them interchangeably to describe some UI. You want to
import A from "a.json" satisfies Tandimport B from "b.json" satisfies Tand haveAandBhave different shapes depending on what was actually in the file.In this scenario, you really do need a type hint on the
import. But this scenario seems sort of niche.Scenario 3: You actually just need a checked type annotation, as if you had written
or
It seems like you're describing Scenario 1 here but it's all sort of adjacent and we'd need to think about which scenarios are adjacent to the request
matthew-dean commentedon Jun 5, 2023
Sebastian Fredriksson Bernholtz (@sebastian-fredriksson-bernholtz)
Ohhhh okay, thank you for clarifying! Yes, if this just worked without having to define a
.d.ts, this would 100% be fine with me and I think would meet requirements, you are correct.To Ryan Cavanaugh (@RyanCavanaugh)'s point, though, there may be some other scenarios where you want contextual typing on the JSON (even if contextual typing was a surprise to me--as it seems like it was worth it in the way
satisfiesoperates). I think you're right that I personally maybe don't need that here, but it seems reasonable that someone might want it on JSON just as much as they would want it on any other defined object.user451421541757324 commentedon May 18, 2026
I think I've run into the same problem.
It's 2026 now and TypeScript has evolved: have you happened to find any workarounds?