Hacker News new | ask | show | jobs
by cryptica 2559 days ago
This is incorrect, merely declaring a type for all your API inputs does not prevent clients (especially a bad actor) from actually sending objects of different types at runtime; and when they do, your code will crash catastrophically. Try it.

You need to do manual schema validation, just like you did with JavaScript. TypeScript adds 0 value. The type only gives you an illusion of safety, which is worse than no safety at all.

4 comments

"Types are useless because they don't version my wire protocol!" is a wholly specious argument. There are a variety of RPC protocols that address versioning in a variety of ways; this isn't a typechecker responsibility. You can generate Typescript interfaces from protobufs if you fancy.

Typescript makes sure that all your code agrees with the protocol definition you have chosen. That alone is immensely valuable.

Protobufs is an antipattern. You can't expect external systems to agree with your system's type definitions. It just gives other people extra work when integrating with your system.

JSON is better because it allows external services to interpret the data in their own way regardless of their type system.

Typescript only checks all that at compile time though. The complaint here is that even a typed json.Parse<T> :(json:string)=>T will not actually give any type checking when you pass it a string containing any schema.
When you type json.Parse<T> you are taking on the responsibility that what is returned is actually T. If you want the type checker's help that you are sure it might not be T, then ask for it: json.Parse<unknown> or json.Parse<Partial<T>> is maybe more accurate, and would force you to write runtime checks. Partial<T> in particular is a great helper type that is often exactly what a lot of want in a json.Parse situation: gives you the field autocomplete you want, but reminds you that they may be undefined.
A sound type system shouldn't allow you to write such a parser function without either you lying in its implementation or having to use unsafe code.

In a year or two, once everyone has switched to use `unknown` instead of `any` in functions which at runtime return… uhm… unknown values, we'll all be much safer. Because we'll be forced to use proper parsers and validators.

And that's what `runtypes` provides.
Used properly, runtime checking and types go well together. Types help you remember whether incoming data has been validated. You can use them to make sure your code does runtime checking exactly once, rather than multiple times or not at all due to changes in different packages that can introduce security bugs. This can make security reviews a lot easier.

I don't know if that's commonly done in Typescript, though? It seems more common in web apps to trust your own server to send you good data.

There are libraries that let you define runtime schema objects which you can invoke to validate incoming data — and they also let you extract static types from those schemas so you don’t have to write things twice:

https://github.com/gcanti/io-ts https://github.com/pelotom/runtypes

"Typescript adds 0 value" to taking user input. Sure. I mean, it's kinda missing a lot of nuance--especially given that it's kind of smart to let-it-crash universe where bad-actor input making your handler crash or fail, so long as it does so safely, is not the end of the world--but yes, you do in fact have to validate user input. You have to do that in every other language too, of course, and stuff like `class-transformer` or `runtypes` or `io-ts` exists to make it easier and safer, but yes. You do.

What about module boundaries between code? What about even the basics of knowing what you're passing into a function is correct?

Me, I write a lot of TypeScript. I've never written code on top of the Node virtual machine as quickly or good or as correct because I'm not stuck resorting to nonsense tests around "well, what do you do if you pass the wrong type to this function?" and I'm not validating internal arguments because TypeScript told you if it was wrong, this is not my bug. Instead I validate user input at the edges (less because of "bad actors" and more to provide helpful messages to the consumers of my libraries and APIs) and then I have a snappy and reasonably correct compiler yelling at me when I do something wrong, immediately after doing so. And because my entire ecosystem, past that scary user-input edge, is also in TypeScript, I am much more sure of the code I'm writing. And, as mentioned, I write it way, way faster.

So if you care about correctness and code quality, why is "well, input validation on the edge is harder" so much more important to you than that sort of thing?