3 ms·
This post is trying to solve a problem you should never have to solve. The function in the post: const cast = <A, B,>(a: A): B => a as unknown as B; should
by culi 1y ago
This post is trying to solve a problem you should never have to solve. The function in the post:
const cast = <A, B,>(a: A): B => a as unknown as B;
should never be used in a professional codebase. The post even admits (though, in my opinion, understates) as much:
> If you're holding it right, these things don't come up, and your code genuinely is much much safer than if you used raw Javascript.
Just wanted to highlight this point I feel needs to be underscored
- mpeg 1y agoEh it's ok to use `as unknown as X` sometimes If you have complex types, it's sometimes the easiest way to do what you want, and it's perfectly safe as long as you are 100% sure that the types are compatible. For example, where you have a fluent-style API where each method modifies the types it's unavoidable to end up using that kind of cast
- aniviacat 1y ago> it's perfectly safe as long as you are 100% sure That was funny to read
- mpeg 1y agoIf you disagree, you're welcome to prove me wrong! To give you an example from a popular open source ts-heavy project: https://github.com/elysiajs/elysia/blob/94abb3c95e53e2a77078bcdd652cd55877b6a697/src/index.ts#L5786-L5826 https://github.com/elysiajs/elysia/blob/94abb3c95e53e2a77078... The `return this as any` there, which effectively casts it to the same type this had, but with the added get route is perfectly safe, it works, and will never be a problem by itself.
- lovich 1y agoIt’s funny because the reason you used a language like typescript is because you want the compiler to be 100% sure that it’s compatible, not relying on human reasoning. If you were going to rely on that anyway, why not just use JavaScript as is and avoid the boilerplate from typescript
- mpeg 1y agoThe code I linked for example results in a web router that is fully type safe. It's not like using js at all, not that I think there's anything wrong with it, if that's your jam.
- horsawlarway 1y agoThis is a great example of "letting perfect be the enemy of good enough". Typescript is "good enough" at it's job. That's a great reason to use it.
- kennywinker 1y agoBecause typescript’s type-checking helps with 99.99%% of the code, and then 0.01% of the time when it doesn’t you use an escape hatch.
- scotty79 1y ago> It’s funny because the reason you used a language like typescript is because you want the compiler to be 100% sure that it’s compatible, not relying on human reasoning. I ever used Typescript because I hoped it would catch some bugs that I didn't (and it did). I never expect it to be 100%. I can't even imagine 100% of what.
- culi 1y agoThe `return this as any` in this codebase was chosen because it was the easiest/quickest route not because it was necessary. Tbh I'm not paid by this company enough to spend time breaking down the correct solution but it would involve validating and discriminating the result of `this.add`. This code is NOT type-safe and will be harder to maintain. Someone will make a change in the future that changes the possible results of `this.add` and TypeScript will not be able to warn you about the consequences of that change.
- matt_kantor 1y agoIt's like how minefields are perfectly safe as long as you know exactly where all the landmines are.
- eyelidlessness 1y agoIt’s not even safe if you’re 100% sure the types are compatible, unless you’re also 100% sure nothing will change that fact. The reason it’s unsafe is because it suppresses the type error permanently, even if whatever factors led to your certainty now change anywhere upstream ever. There are certainly ways to guard against that, but most of them involve some amount of accepting that the type checker produces errors for a reason.
- mpeg 1y agoYes of course the types could change in the future, and the forced cast might cause issues. I wish there was a better way, but this is an acceptable tradeoff. Bear in mind, most changes that could cause issues will still be caught by the type checker in whatever object you're casting to. Obviously it should not be overused where not needed, but it's almost always used in fluent apis because there's no better way (that I know of, at least)
- matt_kantor 1y ago> it's almost always used in fluent apis because there's no better way (that I know of, at least) Got an example?
- mpeg 1y agoYep, I sent one in another comment https://github.com/elysiajs/elysia/blob/94abb3c95e53e2a77078bcdd652cd55877b6a697/src/index.ts#L5786-L5826 https://github.com/elysiajs/elysia/blob/94abb3c95e53e2a77078... This is not the easiest to follow code, but it's very similar to what you'd find in any fluent web router, the idea is that you have say an App class, which has a Routes generic, then on every route you add you compose the return types by returning this as App<Routes & NewRoute>, the thing is in the most simple cases you can probably do this cast directly and it will be fine, but as you add more features (things like extensibility with plugins, ability to merge to other app routes, etc..) you might eventually run into limitations of the type system that require a escape hatch like "as unknown" or "as any" It's not the only case in which you might use it, but I think Elysia is a great example as it does some really interesting things with the type system to provide great DX
- culi 1y ago> For example, where you have a fluent-style API where each method modifies the types it's unavoidable to end up using that kind of cast I think a more concrete example would be necessary but I highly doubt there isn't a more elegant solution using unions and discriminators > it's sometimes the easiest way to do what you want This intention is exactly what leads to unmaintainable typescript codebases imo. Thinking you "know better than TypeScript". TS thinks what it thinks for a reason. Usually that reason is past decisions you made I also don't think it can be perfectly safe. Use a validator if you want it to be perfectly safe
- jmull 1y ago> should never be used in a professional codebase Mostly true. But most system also ingest external data -- that data truly is "unknown" until you parse/validate it. Once you do parse/validate it, you're going to want it to have the correct type. You shouldn't "never" use. You should only use it in limited, constrained ways, and always with other controls that are sufficient to ensure it's correct.
- 0x6c6f6c 1y agoI have usually explained to my teams that you only should be doing this at the boundaries of the system. User input, raw files, request bodies, etc. are places where you would _need_ to determine an actual type for an otherwise unknown value. Even then, a solid validation/parsing library like Zod is a better choice than casting, it is much less error prone at ensuring you have the right type applied. Beyond that boundary, casting with `as unknown` is a massive code smell and often introduces any further use of that data to invalid type errors. I'll refer to this as "force casting", because simply casting "x as T" with sufficient strictness configured will still fail type checking if x is not compatible with T, and that can be fine for certain cases like reducing a complex type or giving an inferred parser type a form that fits your domain types. Developers that force cast too readily can end up creating hundreds or even thousands of type errors in the codebase over time, and they are pretty quickly revealed once you peel away the casting. The same is true when starting from an `any`, where every reference thereon is equivalent to a force cast. There can be some complex typing scenarios that do require it, but even then a good type guard works better even if certain cases can be semantically equivalent, you can write tests around guards as they're just functions, ensuring some logical correctness to the typing. The simplest but maybe odd way to judge whether a casting is used the wrong way is to ask "are you lying to the type system?". If so, how can you better portray that type correctly (validation that determines a correct type for you (e g. Zod) or type guards).
- jmull 1y agoYes, using something like zod at boundaries is the right approach. (Personally, I'm not convinced zod itself is the way to go -- I'm suspicious of its lack of stability. The last thing I want to do is redevelop the boundaries of my system in a few years because zod 4 has been killed off in favor of zod 5, and then again in a few more years for zod 6, etc.)
- kp1197 1y agoIsn't the post more about the interesting capabilities of the language rather than an engineering context?
- culi 1y agoIt's entirely possible that I'm being unnecessarily pedantic, but this doesn't feel like a post about "capabilities" so much as "hacky workarounds". Maybe my years of contributing to undisciplined codebases has made me bitter but TypeScript has ways to solve these problems and it hurts to see more attention brought to hacks than the interesting (and underdocumented) solutions TypeScript already has. Given that roughly half of the documentation is in the damn changelog, TypeScript is especially in need of independent content that highlight features (not hacks) of the language
- braebo 1y agoWhat methods would you want to bring more attention to?