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.
I 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).
Yes, 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.)
> But most system also ingest external data -- that data truly is "unknown" until you parse/validate it.
Absolutely. And there are tools like Zod or e2e type safety solutions to solve this exact problem. And otherwise that data will come in as `any` or already as `unknown` so there will never be a use case for `as unknown as B`. This only happens when the data has already been assigned a type (here, the type is A). Now you're working on recasting it to fix your mistake instead of fixing the core issue of the incorrectly typed data.
The `A as unknown as B` workaround is only relevant when TypeScript believes A and B are so drastically different that they have nothing in common and shouldn't be cast as each other. The reason TypeScript believes that is either because it's true or because you made some earlier mistake.
TL;DR: This article is about fixing your own mistyping and not about typing something that lacks any. It's not about the boundaries of your application but about the internal workings
It'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
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
It’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.
Yes 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)
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
Thanks. I'm not going to be very specific here because I'm too lazy to dig into that giant type, but if they want that method implementation to work without type assertions then the `add` method would need to be typed as an assertion function[1] so the type system can understand that it narrows its argument[2].
[2]: Doing this isn't safe anyway because it mutates an object in a type-relevant way while there may other variables referring to it (the safe thing to do is return a new `Elysia` instance from `get`), but that's beside the point.
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.
The `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.
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.
If you were going to rely on that anyway, why not just use JavaScript as is and avoid the boilerplate from typescript
> 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.
> 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
I think the title undersold it. I would call it "a nonexhaustive set of abuses of casting in TypeScript"
I actually thought that calling out the `is` operator was useful, I have a generic utility method I call `filterFalsy` which takes `T | undefined | null` and returns `arg is T`, which seems decently safe, but it's interesting how it would fail when asserting between two types.
Unconvention 2 made me vomit, inlining a function like that would never pass code review, indeed if you remove the mutator from being declared inline it restores type soundness as you would expect
Unconvention 3 is important to learn, TS "duck typing" is not resilient to usage of spread/Object.(keys|values|entries)
Unconvention 4 is why I argue to use `callback: () => unknown` whenever you won't use the return type, it is the least restrictive and also implies to stay the hell away from that return value.
Funny, just yesterday I found myself casting in a way I'd never seen before:
const arr = ['foo'] as ['foo']
This wound up being useful in a situation that boiled down to:
type SomeObj = { foo: string, bar: string }
export const someFn = (props: (keyof SomeObj)[]) => {}
// elsewhere
const props = ['foo'] as ['foo']
someFn(props)
In a case like that `as const` doesn't work, since the function doesn't expect a readonly argument. Of course there are several other ways to do it, but in my case the call site didn't currently import the SomeObj type, so casting "X as X" seemed like the simplest fix.
Er, my justification was that the code in question was meant to be minimally demonstrating someFn, and adding an import or a verbose type seemed to distract from that a little.
But mostly it just gave me a chuckle. I tried it because it seemed logical, but I didn't really think it was going to work until it did..
Sigh, the abuse of void was particularly eye-opening for me. If one really must do this - and I can think of a couple of cases where one might mostly around progressively porting old codebases to typescript - I'd strongly prefer the simple `a as unknown as B;` as one can easily grep for ` as unknown as ` to find your crimes.
it's gradually and structurally typed and I think that's what makes it great. I also disagree that it's vague. Nowadays you can even have typesafe regex
Well for one, using double equals is evidence you don’t know JS well enough. Many years before Typescript came along the JS community was prohibiting “1” == 1 style coercions.
I believe satisfies will narrow the type const infers. It won't lose any information, so you can check it satisfies a broader type without stopping it being used as a narrower one, but it will narrow down the inference if given (but you can of course widen it back out with an explicit typing).
This post is trying to solve a problem you should never have to solve. The function in the post:
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
> 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.
I 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).
Yes, 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.)
> But most system also ingest external data -- that data truly is "unknown" until you parse/validate it.
Absolutely. And there are tools like Zod or e2e type safety solutions to solve this exact problem. And otherwise that data will come in as `any` or already as `unknown` so there will never be a use case for `as unknown as B`. This only happens when the data has already been assigned a type (here, the type is A). Now you're working on recasting it to fix your mistake instead of fixing the core issue of the incorrectly typed data.
The `A as unknown as B` workaround is only relevant when TypeScript believes A and B are so drastically different that they have nothing in common and shouldn't be cast as each other. The reason TypeScript believes that is either because it's true or because you made some earlier mistake.
TL;DR: This article is about fixing your own mistyping and not about typing something that lacks any. It's not about the boundaries of your application but about the internal workings
Isn't the post more about the interesting capabilities of the language rather than an engineering context?
It'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
What methods would you want to bring more attention to?
Eh 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
It’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.
Yes 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)
> it's almost always used in fluent apis because there's no better way (that I know of, at least)
Got an example?
Yep, I sent one in another comment 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
Thanks. I'm not going to be very specific here because I'm too lazy to dig into that giant type, but if they want that method implementation to work without type assertions then the `add` method would need to be typed as an assertion function[1] so the type system can understand that it narrows its argument[2].
Here's an example: https://tsplay.dev/w8y9PN
[1]: https://www.typescriptlang.org/docs/handbook/release-notes/t...
[2]: Doing this isn't safe anyway because it mutates an object in a type-relevant way while there may other variables referring to it (the safe thing to do is return a new `Elysia` instance from `get`), but that's beside the point.
That's really interesting with the assertion function, I've not seen that done much, thanks!
> it's perfectly safe as long as you are 100% sure
That was funny to read
If 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/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.
The `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.
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.
If you were going to rely on that anyway, why not just use JavaScript as is and avoid the boilerplate from typescript
The 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.
Because 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.
> 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.
This 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.
It's like how minefields are perfectly safe as long as you know exactly where all the landmines are.
> 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
I think the title undersold it. I would call it "a nonexhaustive set of abuses of casting in TypeScript"
I actually thought that calling out the `is` operator was useful, I have a generic utility method I call `filterFalsy` which takes `T | undefined | null` and returns `arg is T`, which seems decently safe, but it's interesting how it would fail when asserting between two types.
Unconvention 2 made me vomit, inlining a function like that would never pass code review, indeed if you remove the mutator from being declared inline it restores type soundness as you would expect
Unconvention 3 is important to learn, TS "duck typing" is not resilient to usage of spread/Object.(keys|values|entries)
Unconvention 4 is why I argue to use `callback: () => unknown` whenever you won't use the return type, it is the least restrictive and also implies to stay the hell away from that return value.
Fun article.
Funny, just yesterday I found myself casting in a way I'd never seen before:
This wound up being useful in a situation that boiled down to: In a case like that `as const` doesn't work, since the function doesn't expect a readonly argument. Of course there are several other ways to do it, but in my case the call site didn't currently import the SomeObj type, so casting "X as X" seemed like the simplest fix.Why not use annotation instead?
Didn't occur to me, that's certainly more defensible! Though maybe less humorous.
Or: cons foo = [‘foo’] as const;
> In a case like that `as const` doesn't work, since the function doesn't expect a readonly argument.
Ah, missed that. Sorry.
I don’t get this? why do I need to say as const?
`as const` is a special annotation that lets the TypeScript compiler infers the more specific type `["foo"]` instead of `string[]`.
As const creates a typed tuple instead of a typed array
You could also do
That's also different: "foo"[] as opposed to ["foo"] or readonly ["foo"].
I generally put lint rules to prevent casting, why cast here instead of declaring `props: (keyof SomeObj)[]` or `props: Parameters<typeof someFn>[0]`?
Er, my justification was that the code in question was meant to be minimally demonstrating someFn, and adding an import or a verbose type seemed to distract from that a little.
But mostly it just gave me a chuckle. I tried it because it seemed logical, but I didn't really think it was going to work until it did..
Sigh, the abuse of void was particularly eye-opening for me. If one really must do this - and I can think of a couple of cases where one might mostly around progressively porting old codebases to typescript - I'd strongly prefer the simple `a as unknown as B;` as one can easily grep for ` as unknown as ` to find your crimes.
Can't you change settings in ts to make it more 'strict' than this ?
This why I find Typescript frustrating.
It really should be called "vaguely typed script"
it's gradually and structurally typed and I think that's what makes it great. I also disagree that it's vague. Nowadays you can even have typesafe regex
"Not really typed script"
"as much typing as you want"-script
TypeScript is incredibly safe if you use strict tsc settings and ts-eslint strict presets.
If it were this strict out-of-the-box, it probably would have been hated since most devs don't really want to deal with static typing.
https://typescript-eslint.io/getting-started/typed-linting
> TypeScript is incredibly safe
I expect this comment is from a person who has not used strongly typed languages extensively. Is that true?
When "1" == 1, and "1" can cross a wire and be assigned to a number, when there are no ints...
Perhaps I have not given Typescript enough rope, I only spent eighteen months immersed
It is not shit, but it is very outdated IMO.
I have been known to be wrong, I admit, but all these "unconventional " casts, together with the standard ones, are exactly what frustrated me
Well for one, using double equals is evidence you don’t know JS well enough. Many years before Typescript came along the JS community was prohibiting “1” == 1 style coercions.
I'd call it an "actually usefully typed script for people without a stick up their butt" but that's a mouthful. TypeScript will do.
On the other hand, you can run Doom in TypeScript's type system.
Another one:
Surprised the `satisfies` operator wasn't called out
How would that work? My understanding is that satisfies doesn’t change anythings type, it just provides an additional type validation check.
I believe satisfies will narrow the type const infers. It won't lose any information, so you can check it satisfies a broader type without stopping it being used as a narrower one, but it will narrow down the inference if given (but you can of course widen it back out with an explicit typing).
That doesn't cast or change compile time type right?
[dead]