Hegel is basically a less mature version of Flow. I preferred Flow over TS for the longest time, but let's face it – TS won in terms of absolute adoption, with very little prospect for anyone to take over.
Hegel is basically a less mature version of Flow. I preferred Flow over TS for the longest time, but let's face it – TS won in terms of absolute adoption, with very little prospect for anyone to take over.
What everyone who asks for runtime checking actually wants is:
- define once
- declaratively
- with clear validation and deserialization at boundaries where data types aren’t known
- without overhead for internal types which are well known
- without an additional build step like codegen
That is type guards! They just have to be granular and composable, and well tested at a granular and composable level. Libraries like zod and io-ts have shown how to do it. You can just use them, or many others like them, you don’t need anything new. Or you can pretty trivially build your own custom solution if they don’t scratch your particular itch.
You aren’t missing seamless runtime checks. You’re just not using them.
What everyone wants: Autogenerated type guards by class.
What everyone gets: “Remains out of scope.” and need to use other libraries. The compiler can do this significantly better (more ergonomic, less time, etc) than 3rd party libs. TS devs really should reconsider this feature.
I’m quite sure people are asking for that, but I’m quite sure they’d regret wanting it if they got it. To the extent it’s desirable, it’s a thing that should be in TC39, not in TypeScript. The wailing and gnashing of teeth over when or whether or how type coercions happen is already mythical for the underlying language. Adding it to a compiler that’s increasingly not used to compile but only to type check is gonna add ten years of gripes about JS tooling nonsense to every single discussion.
They have considered this feature. Type guards are the solution and they’re a really good solution given the circumstances.
Flow had [flow-runtime](https://github.com/gajus/flow-runtime), which was nice. There were/are a few half-baked solutions in TS ecosystem, but nothing as mature as flow-runtime was. However, I wish that TS supported this natively.
I think what OP might've meant by run-time checks is auto-generating code that wraps every unpredictable/non-guaranteed boundary with a run-time type check. So if you have some data with the "any" type data being turned into a type or a use of the "as" keyword (e.g. `someType as anotherType`), then add a run-time check that throws an error at that boundary (instead of the code erroring out elsewhere, or resulting in incorrect behavior). Something like
[1] https://www.typescriptlang.org/docs/handbook/advanced-types....
That’s a runtime type check! Any equivalent you might bake into TypeScript would do the exact same thing, but it would do it rigidly in ways people don’t expect (being strict about input types, ie “do what I say not what I mean) or rigidly in ways people don’t expect (“do what I mean as long as you mean exactly what we allow”). Again, fine grained type guards as implemented by zod/io-ts/etc solve this problem and either give you all you need already or demonstrate exactly how you can use existing language features to achieve exactly what OP and anyone else asking for runtime types wants.
https://www.typescriptlang.org/docs/handbook/release-notes/t...
Type assertions match what you describe better - they are ways to override the compiler's opinion about the type of a variable or expression. They are, as you say, unsafe in the sense that the compiler has no way of knowing if this assertion is true or not.
Sadly this isn't true: https://tsplay.dev/WvGArw
I understand that adding something like zod is out of scope for tsc. However, type guards alone are not the full answer either.
import macro { Deserialize } from 'ts-serde'; // I made up "import macro" syntax
import { parse } from 'ts-serde';
#[Deserialize] // I borrowed this syntax from Rust
interface Foo {
bar: string;
baz: number;
qux: Record<string, string | null>;
}
Now I can do something like:
const response = await fetch('https://www.example.com');
const foo = parse<Foo>(await response.json());
This is a much nicer typescript world to live in. I can use vanilla interfaces (or even types). I do not have to learn a separate syntax for runtime checks (such as zod, io-ts, runtypes, etc). If I want to switch from one macro implementation, my scope of changes is the annotation on the interface and the "parse" function. That is much, much easier than switching from io-ts to zod.I can define types using io-ts, infer a true typescript type from that to put in my d.ts files to get full ts type checking, and also have run time type checks, all from the same single definition.
The initial learning curve is admittedly a little steep, but once you have it down it's a breeze to use, and delightful.
Previous discussion: https://news.ycombinator.com/item?id=31663298 - it's downright mindblowing that all this seems to be the work of primarily a single developer.
For a less intrusive solution, https://github.com/jquense/yup is a great library to reach for whenever you're defining the shape of a network-transmitted object and don't want to introduce compilation stages.
https://github.com/colinhacks/zod
You’re welcome.
(I use this library every day. It has changed my life.)
https://github.com/ar-nelson/spartan-schema
It does the same thing as Zod, but is much smaller/simpler and its types always have a JSON representation.
Zod is filling a gap in the language - runtime type checking. It is not the only one doing so, but the fact that there are several such solutions clearly indicates that there is a general need for it.
The language doesn’t have, or at least makes not having a primary goal, any runtime. “The” language is JavaScript. TypeScript is just annotations of it. Really good annotations. So good that you can build them out of reliable runtime checks which are type guards, without even writing a single type definition except to infer from them. Zod is filling a gap in JavaScript. TypeScript is facilitating that.
However, what is or isn't the goal of a language is subject to change. Libraries like Zod are trying to filling a gap in js, sure. But we are arguing that that gap is better filled by the typescript - if that requires expanding its goal or doing things that don't fit in current scope - so be it.
As to why it is better: Currently using zod requires that the author of the types uses zod's API. If they didn't anticipate that the types they are writing would need runtime checks, or prefer some other schema validation library than the one you prefer - as a consumer you need to now redefine all of these types using zod and keep them in sync. Compiler can help, sure but its still quite some work.
If typescript supported this natively, you could import some random library and use its types for runtime validation - that is not possible today, and it can't be possible until ts has first class runtime type validation.
People would also not have to learn the ts syntax and then also the zod api to achieve it.
Languages like dart demonstrate that this is practical.