Non-goals:
…
5. Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.
Fetch returns “any” meaning you can’t trust the data you received is actually the data you expected. Bugs from this mismatch will be many lines away (on first use) and more difficult to find. Because of this “goal of the language” you cited, there’s no built-in way to validate any data at runtime. In nearly any other typed language I have some deserialization mechanism. Not so in Typescript!
This decision led to more bugs in our codebase than any other. The compiler actively lies to you about the types you’ll have at runtime. The only solutions are codegen or writing validators to poorly approximate what Typescript should give us for free.
The correct type for values you don’t know the type of (like the response of an API call) is “unknown”.
TypeScript does not provide the facilities you describe because there is not a one-size-fits-all solution to the cases that are possible and common in JavaScript.
It is left to the developer to decide how to validate unknown data at the boundaries of the API.
There are third party libraries that facilitate this in different ways with different trade-offs.
The compiler actively lies to you about the types you’ll have at runtime.
I find this to be rare if you are using strict mode with proper TypeScript definition files for your platform and dependencies. Usually the lie is in your own code or bad dependencies when an “unknown” type (including “any”) is cast to a concrete type without being validated.
In nearly any other typed language I have some deserialization mechanism.
Could you provide examples? I either don’t understand or I disagree.
This can't be solved by static analysis - anything that crosses i/o boundary has to be asserted, refuted or predicated at runtime and you have libraries for it ie. [0] which doesn't throw (based on refutations which can be mapped to predicates without much cost and assertions) or [1] which throws (based on assertions).
Predicates are the most performant but won't give you any indication on why it failed (ie. some nested field was null but was expected to be number etc).
Refutations is great sweet spot as it's fast while giving information about error.
Assertions are slow, but more often than not you don't care.
You can map between any of them, but it doesn't make much sense for mapping ie. assertion to predicate as you'd be paying cost for nested try/catch while dropping error information.
The complaint is that Typescript not emitting any of the type information for the runtime means every library must reimplement the whole TS type system.
Yes, that's true, they could support emitting metadata with explicit keyword which would help and wouldn't bloat anything implicitly, they already do emit code for enums for example.
Personally I'm fan of not introducing new language that runs at comp time, just use the same language to have macros and operations on types for free - just like Zig does it.
Typescript type system is already turing complete so it's not like they'd be loosing anything there.
Regarding runtime type checking, if you were to write something that can handle the total space of possible TS types, you would end up with incredibly complex machinery. It would be hard to make it perform, both in terms of speed and bundle size, and it would be hard to predict. I think Zod or perhaps https://arktype.io/ which target a reasonable subset are the only way to go.
This was driving me nuts in a project with lots of backend churn. Runtime type validation libraries like typebox and zod (I like typebox) can really save your bacon.
The downside is the underlying types tend to be more complex when viewed in your IDE, but I think it's worth it.
type Identity<T> = T
// This can be made recursive to an extent, alas I’m on mobile
type Merge<T> = {
[K in keyof T]: Identity<T[K]>
}
type ReadableFoo = Merge<UnreadableFoo>
You should take a look at https://zod.dev/ if you haven't already - it's a library for runtime parsing that works really well for your use case.
Types are inferred from the schema though personally I like to handwrite types as well to sense check that the schema describes the type I think it does
I’ve used zod and every other schema validator available for this. Some problems:
1. Types are not written in typescript anymore. Or you have to define them twice and manually ensure they match. ReturnType<typeof MyType> pollutes the codebase.
2. Types have to be defined in order, since they’re now consts. If you have a lot of types which embed other types, good luck determining that order by hand.
3. Recursive types need to be treated specially because a const variable can’t reference itself without some lazy evaluation mechanism.
TS could solve all of this by baking this into the language.
1. You can just use `export type Foo = z.infer<typeof fooParser>` in one place and then import Foo everywhere else, without using z.infer everywhere else
2. Use let and modify your types as new ones become available - union them with a new object that contains the new property you need
3. How often are you making recursive types?
I agree that all of this could be made easier, but zod is the best we have and great for most normal usage. The reason TS doesn't want to make this available at runtime is that it means so many changes they make will become breaking changes. Perhaps one day when there's less development on TS we'll see this get added
Including runtime checks would also have performance implications.
I really enjoyed using myzod (more performative, simple, zod) for awhile, but recently I’ve been using Typia, which is a codegen approach. I have mixed feelings about it, and from my own benchmarking it’s performance seems overstated, but the idea is sound: because we know the type, we can compile better, type-optimized serialize/deserialize functions.
As for not littering the codebase with runtime checks, it may be worth reiterating to the person above that you really should only do type determinations at the I/O edges: you parse your input, and it becomes known from then onwards. You runtime type-check your output, and its requirements propagate upwards through your program.
Pragmatically, your interest is why I was mentioning typia, which does what you are describing: opt-in parser/stringify/mock-gen codegen derived from typescript.
I think it’s reasonable enough to allow other people to focus on runtime behavior. There’s still a lot to do to model js accurately.
In my personal opinion, the ideal ts would be one where you just write regular js, and the compiler is able to check all of it for correctness implicitly. That would require runtime validators etc to be explicitly written, yes, but you could “just write js” and the correctness of your program could be proven (with guidance to make it more provably correct when it is not yet).
It would be a lot nicer if it instead returned some JsonType that’s a union of all the possible JSON values. Anyone know if there’s a good reason why it doesn’t do that?
There's a big discussion about this: https://github.com/microsoft/TypeScript/issues/1897. The benefit seems extremely limited to me. Valid JSON is obviously a subset of `any`, but I can't think of a situation where that particular specificity provides any value. Can you?
The value is when you’re parsing the JSON afterwards. It’s good to know you can match it exhaustively -- each value is either a Record<string, Json>, Json[], string, number, Boolean or null, and nothing else.
Edit to add: I think “any” is almost always a big cop-out because you couldn’t be bothered figuring out the correct type, and it often causes problems (loss of type coverage) further down the line. I admit I do use “any” in my own code when I really need to, but a library should work harder to avoid it, and the standard platform typings should work harder still.
Interesting, I think it indeed falls under "emit different code based on the results of the type system" even though - thank you for the link.
I'm not sure if there is a) any "programming pattern" that can avoid this without other drawbacks and b) if there is any problem with emitting different code based on the types (at compiletime).
I suppose it could lead to breaking behaviour if the typesystem is changed, since it now can impact runtime code. Personally, I think this would be more than worth it, but maybe the typescript team has a different opinion or other reason.
They’re suggesting outputting runtime type information by emitting different code based on the type of the value passed to their hypothetical “Type.keys()” function.
This is why Angular for dependency injection as well as some ORMs require non-standard Typescript emitted during compile time for a long time now. It looks a quite locked-up conflict.
The Reflect.defineMetadata API and the long-supported decorators syntax come from very early versions of Typescript when Typescript was (maybe) more actively trying to steer the direction of ECMAScript by implementing features that were Stage 2 proposals.
Typescript only got official ECMAScript decorator support in the recent v5. ECMAScript decorators only got to stage 3 in April ‘22.
But decorator syntax is just a kind of syntax sugar over passing a function through another function, and you can do that today to achieve runtime type information (see zod etc). Zod could be rewritten using decorator syntax and still be “just JavaScript” while providing compile-time type support.
The distinction being that supporting ECMAScript features is a goal for Typescript, but they were perhaps too aggressive early on in investing in decorators and the Reflect Metadata API. They had the wisdom to put these behind “experimental” flags, but I think they got quite popular within the typescript community due to the early adoption of both Typescript and both those features by Angular, which was really the only major lib using TS for quite some time.
> Typescript only got official ECMAScript decorator support in the recent v5. ECMAScript decorators only got to stage 3 in April ‘22.
Yah, they've been out for ages. It's quite surprisingly how 1. long it's taken ECMA and 2. how quickly TypeScript took advantage of decorator syntax to improve TypeScript. I'd say it's a definite win for us people who love decorators.
> But decorator syntax is just a kind of syntax sugar over passing a function through another function, and you can do that today to achieve runtime type information
I'll have a look at Zod, thank you! I have to admit I like the simplicity of decorators; I'm playing with Dependency Injection and, while the loss of parameter injection is a bit disappointing, there are ways to work around it, e.g.
@Injectable([Dependency])
class Service {
constructor (private dependency: Dependency) { }
}
> [...] features by Angular, which was really the only major lib using TS for quite some time.
Definitely. Although it'd be interesting to see how Angular handles the transition away from parameter injection; there's an open issue about it on their GitHub, but from what I can see none of the core members have spoken about it yet.
<https://github.com/angular/angular/issues/50439>
The main proposal from a community member is to replace them with the Service Locator pattern (ew). Thankfully someone in-thread provided them with a little wisdom regarding why that's a terrible idea. Here's hoping Angular keeps a nice API.
https://github.com/Microsoft/TypeScript/wiki/TypeScript-Desi...
Edit: