Alternatively, use a runtime validator the provides good type inference out of the box so you're still only declaring your types once.
If typescript emitted type information, we'd be able to automatically get runtime validation at the boundaries of our apps.
And even if you are--that might also involve ensuring their tsconfig.json is compatibly similar to yours. Otherwise the compiler might allow them to return null instead of the object they say they return, among potentially many other "gotchas" that are bound to appear.
EDIT: Though I think I do agree with you, ultimately. Runtime type checking imposes non-negligible costs and complexity that still theoretically should be able to be guaranteed at compile time for cases where one isn't validating untrusted user input.
Did you read the article? I think that's the point. The premise is Typescript should be responsible for solution to runtime validation against Typescript not third party hacks.
Well that, and getting rid of the silly "must be able to strip types without processing them" design ideology would also enable stuff like typeclasses.
Right, but now my type information for inbound data must be in two places: the typescript type, and the validation schema. And heaven forbid I make a mistake and those two become out of sync.
Yes, I can use something like zod to define a schema then infer the type from it, but those inferred types are often … suboptimal to work with.
When I produce a type definition for a nested schema, I'll produce types for individual pieces.
So
type Schema = { foo: Foo; bar: Bar; baz: Baz[]; }
type Foo = { red: Color; white: Color; blue: Color; }
type Color = { r: number; g: number; b: number; }
type Bar = {...} type Baz = {...}
I couldn't find a clean way to decompose the type that z.infer<Schema> produced. It would give me a single type object that was deeply structured. For a pretty flat-simple small schema, z.infer<> was totally fine.
For the more complicated schema it wasn't _terrible_, I still used it. I made it work, but it definitely wasn't the dev experience I was hoping for.
I think something that went the other way would be much preferable for me. I'd rather define the typescript type, then have some validate<Schema>() function that was available to me.
Basically, I think it's _easier_ to get a generated validate() function to play nicely with the rest of my code, than it is to get the inferred type to play nicely with the rest of my code.
It is also the most frequent thing where junior developers get stuck. TS tooling doesn't make it very easy to work with large extracted types though it is head and shoulders above other mainstream languages. The error messages often become incomprehensible walls of text once your types are Complex enough, and then tsserver will just truncate the type info in pop overs making them useless.
I am personally OK to live with all of the above. My single issue with zod is that it is not easy to use if you don't own your types. If your types are coming from a third party lib you don't have an easy path to derive runtypes from them. If the concept of runtypes was supported by the compiler itself, this could have been possible but as a userland library zod can't handle this easily.
Imagine a Zod schema like:
const UserLocation = z.object({ address: Address, coords: Coords })
where Address, Coords are other zod schemas.
Now if I infer a type like:
type IUserLocation = z.infer<typeof UserLocation>
Th inferred IUserLocation is something like:
type IUserLocation = { address: { city: string, country: string, ... }, coords: { lat: string, long: string } }
The extracted type does not refer to separate Address, Coords type, it is a single complex type that represents the complete nested structure.
So if I do something like:
const userLocation: IUserLocation = { address: { ... }, coords: { lat: 0, long: 0 }}
The error message is complex because it (effectively) says that coords.lat was expected to be a string from IUserLocation["coords"]["lat"] but it was number
However, if I define a ts interface directly:
interface IUserLocation { address: IAddress coords: ICoords }
now any errors reported against this interface will be more comprehensible because they will (effectively) say that coords I am providing does not comply with ICoords.
This is a contrived example, but this becomes more complex when you have deep compositions of zod types.
This issue of typescript not using well named intermediate types when types are "extracted" from schema definitions like zod is what makes the error messages complex.
This would not apply if typescript was to provide runtime type checking for interfaces defined in typescript (as opposed to them being inferred) because ts will have a way to "know" about these intermediate types. Then the type errors would not have to be always reported against the complex top level type.
I just realized that I am working around this problem by explicitly setting a `z.Schema<...>` type: https://github.com/kristiandupont/kanel/blob/main/example/mo...
But I agree with you that there is room for improvement.
Thanks for maintaining kanel btw. We used to use this in a previous role alongside knex. It was very useful.
[1] https://lorefnon.me/2022/06/25/generating-api-docs-for-zod-t...
interface Person { firstName: string; lastName: string; }
const PersonSchema = z.object({ firstName: z.string(), lastName: z.string(), }) satisfies z.Schema<Person>;
My complaint about writing the schemas twice isn't the busywork (which is, like, annoying, but whatever I'll live). But the concern about small discrepancies between the two causing problems later.
I'll definitely use this going forward.
To the extend that TS's type-system is unsound, and everything may crash at runtime at random anytime.
schema Example { ... }
cosnt example: Example = new Example(unkonwnData)
Where a schema applied runtime validation based on declarative syntax and which typescript could use to derive a `type` from
The only place you need runtime type enforcement (when you're writing your own code in TS) is for validating third party data at the point where you're ingesting it into your own code. Once it's in there, it is type safe if you used TS to compile your code to JS, because your function calls and data copies and everything else that moves data around was defined in terms of compatible shapes.
And we already have runtime validation libraries to cover that validation step. So many of them.