-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
satisfies
should work with imported / referenced values
#54488
Comments
It seems like this does what you want? What falls short here? import book from 'book.json';
book satisfies BookResource; I don't think we want to mix-and-match TS and ES syntax in |
I think this is probably morally a duplicate of #32063 (to import a json module " (@RyanCavanaugh the |
@RyanCavanaugh Here is a reduced test case: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgEIHt0GsAqBPABxQG8AoZZMQiALmQHIAjTLe0gX1NIXRAGcwyGJmQBeZGQpUidJizadSw9Mj5wwwPjGAQ+aFviJcgA
|
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 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. |
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? |
@RyanCavanaugh Well, what I mean is more that the
The fact that |
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. |
@RyanCavanaugh Correct. I'm not suggesting changing |
i.e. I would be overjoyed if this worked: import book from 'book.json';
book satisfies BookResource; If that's all it took for |
@RyanCavanaugh @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 |
I think these are separable and both valuable. The scenario of a middle ground between the entire blob being |
If we had 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. |
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:
|
Really, this issue is more fundamental than objects or JSON, as demonstrated by this playground example, in which the behavior of So, in addition to this issue, which is like a feature request, I would suggest there's also a Also, can I point out that the TypeScript documentation has this to say:
That was how I understood |
#47920 is a big discussion but it explains that one intended use of But at no point was const x = [1, 2] satisfies [number, number]; // okay is the intended use case, and is fundamentally different from const x = [1, 2];
x satisfies [number, number]; // nope where instead of providing context for the value So I could see any of import book from 'book.json' with { satisfies: BookResource };
import book satisfies BookResource from 'book.json';
import book from 'book.json' satisfies BookResource; being implemented since you're declaring the variable in the same statement with your desired type, but import book from 'book.json'; book satisfies BookResource doesn't look feasible. |
Thanks for the additional info. I do hope the documentation will be reworded. One thing, this: let x = [1, 2] satisfies [1, 2] Results in a type of x of let x = 1 satisfies 1 | 2 ...results in a type of 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 let x: 1 | 2 = 1 I dunno, still trying to wrap my head around the intended behavior. |
@matthew-dean 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 type FooA = Array<{ variant: "primary" | "secondary"; num: number }>
type FooB = Array<{ variant: "primary" | "secondary"; num: 1 | 2 }>
const foo = [
{ variant: "primary", num: 1 },
{ variant: "secondary", num: 2 },
] as const satisfies Readonly<FooA> satisfies Readonly<FooB> 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: const fooA: FooA = [...foo]
const fooB: FooB = [...foo] Notice my usage of type annotations over the The purpose of the
In 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. let first = 'one' // type "string" is inferred
let second = 'one' satisfies 'one' | 'two' // type "string" is inferred
let third: 'one' | 'two' = 'two' // type "'one' | 'two'"
third = first // error because TypeScript works on the type of "first" ("string"), the value ('one') is not considered
function tryValues(val: 'one' | 'two') {
console.log(val)
}
tryValues(first) // error because TypeScript works on the type of "first" ("string"), the value ('one') is not considered
tryValues(second) // error because TypeScript works on the type of "second" ("string"), the value ('one') is not considered
let demoCoercion = {
something: 'one'
} satisfies { something: 'one' | 'two' } // type "{ something: 'one' }" is inferred
tryValues(demoCoercion.something) // works because type of "something" is "'one'", the value is not considered
import book from "book.json" // say that book has value { author: "matthew" }, then book will be of type "{ author: string }"
const foo = book satisfies { author: "matthew" } // error because TypeScript works on the type of "book" ("{ author: string }"), the value ({ author: "matthew" }) is not considered 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). type T1 = typeof ({
variant: "primary",
num: 1,
}) // error: identifier expected
type T2 = typeof "primary" // error identifier expected
let t1 = "primary" // t1 is inferred to be of type "string" but that is NOT the type of "primary", as we can see on the next line
let t2: "primary" = "primary" // this would give error if type of "primary" was "string" Now we are getting closer to explain some of the inconsistencies with the 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 You're not wrong though, there are inconsistencies in how There are also cases where let t3 = "test" satisfies string satisfies "test" // no error, i.e the resulting type from "satisfies string" is not "string", it must still be undefined or literal type
let t4 = { a: "test" } satisfies { a: string } satisfies {a: "test"} // error 'string' not assignable to '"test"', ie the resulting type is defined as { a: string }, not undefined or literal type All of the code has been tested in TypeScript version 5.0.4 (VSCode) and 5.1.3 (TS Playground). |
Have folks read the very long discussion about this behavior in the original |
@RyanCavanaugh Ah I see. A number of people objected against contextual typing for reasons of confusion that I encountered, and you argued for |
@sebastian-fredriksson-bernholtz
Not really. To add more context, let me make it more clear. I refactored some sample data files from In doing so, I, of course, had to remove the So now we have data with no contract. 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. 🤷♂️ |
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? |
@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 |
@matthew-dean
// narrows type, eg type { author: readonly "matthew" } instead of type { author: string} in original
import book from 'book.json' as const
// eg type BookResource = { author: "matthew" | "seb" }
// no longer any error after type narrowing through as const,
// contract is being validated
const foo = book satisfies BookResource // type { author: readonly "matthew" }
const bar: BookResource = book // type { author: "matthew" | "seb" } 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: import book from 'book.json' as const
book satisfies BookResource // I don't think this has any effect except typechecking Or you can do it inline where it's being used import book from 'book.json' as const
const myFunc = (myBook: BookResource) => {}
myFunc(book) // would throw error if book does not match BookResource |
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 In this scenario, you don't need Scenario 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 In this scenario, you really do need a type hint on the Scenario 3: You actually just need a checked type annotation, as if you had written import x from "./foo.json";
const y: T = x; or import x from "./foo.json" as const;
const y: T = x; 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 |
@sebastian-fredriksson-bernholtz
Ohhhh okay, thank you for clarifying! Yes, if this just worked without having to define a import book from 'book.json' as const
book satisfies BookResource To @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 |
Suggestion
Right now,
satisfies
works on values and object literals right after they're declared. However, there are use cases whensatisfies
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:
⭐ Suggestion
In short, I'd like to be able to do this:
or, alternatively:
and have it work equal to:
📃 Motivating Example
I have this structure in my book data JSON file:
And I have a type (interface) like:
(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:
Now, I wasn't sure if this should be marked as a bug or feature request, because this works:
In 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 ofstring
) 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:
In TypeScript / JavaScript terms, these should be equivalent statements, and TypeScript should be able to
foo
satisfies the type ofBookResource
bar
to be oftype: "book"
vstype: string
, just as it would ifsatisfies
immediately followed the value.1st note: obviously I could just use
as BookResource
instead ofsatisfies 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.The text was updated successfully, but these errors were encountered: