Let say you have two types of users in your app, regular users and superUsers.
type RegularUser = {
id: string;
username: string;
email: s...
For further actions, you may consider blocking this person and/or reporting abuse
I have to disagree with you that the in operator is an anti-pattern in Typescript.
The
in
operator checks at runtime if the property is available somewhere in the property chain. TypeScript correctly infers the type ofuser.userName
asunknown
. However, if you add a return type to thegetUserName()
function, you will get the expected type error:TypeScript Playground
In my opinion, if you rely on types, you should completely type input and output, especially if you use dynamic accessors like
in
orObject.hasOwn()
.I personally prefer to omit return-type annotations and let TS infer the correct type when possible β at times, this even yields more correct results, because the return type annotation can act as a sort of type-cast under certain circumstances. Without the return type, you would still get the expected error as soon as you tried to actually do anything useful with the returned value:
This is an excellent point you raise.
I think this is a strong example of why using explicit return types is good.
There is more room for error with implicit return types.
π― on return types. Your consumers should not be what enforces and validates your contact, but rather be explicit of what it's expected to return.
Maybe your assumption is that the function should return void if the property wasn't found. If consumers are utilizing this then this is valid code.
Exhaustive unit tests, and code reviews are the winners in this case, especially a typo too. If you wrote 100% test coverage there's a logical branch in the code which returns void. If you write the test covering that scenario to get 100% coverage, then you've either realized your code doesn't do what you want, or you've now documented that it's expected behavior.
Although your example is correct, Typescript's inference with the "in" operator is sometimes unsound. Here's a modified playground to demonstrate what I mean: Playground
So it's important to be aware of the danger of "in" in Typescript, but it's only dangerous in certain circumstances.
Sorry, I can't follow why your example should be wrong:
You defined the
isSuperUser
as typetrue
and you checked if it exists on the given object with thein
operator. TypeScript infers the type astrue
(because it exists) and lets you calltoString()
on it.That all sounds right to me. Why do you think the type should be
unknown
in this case?I think the best way to fix the first example is to add
isSuperUser?: false
to RegularUser.compiler does not complain about isSuperUser, and we can see that newly added roles should have isSuperUser if we add a new role to the User type without isSuperUser, the code will emit an error.
Thank you for your comment.
Why would you make the
isSuperUser
optional on the RegularUser ?From a business perspective, I would use a structure like
roles: ("ADMIN"|"GUEST")[]
for permission checking.However, from a type perspective, flags like
isXX
are not a problem.This was my thought too.
Even if we REALLY needed separate types, I would possibly still add 'isSuperUser: Never' to the User type or something similar.
I agree
in
can be confusing for type assertion in TS and I don't think I use it at all these days. The code you write isn't wrong in itself, it's just a bad assertion in the first place. But with other patterns thanin
TS can warn you better indeed.Something like :
Β
Will yield much better safety indeed.
I think it would be nice if you could elaborate that the reason why the following compiles
is because you can pass this into
getUserName
Which is valid because, as you mentioned, typescript types are structurally typed. (Edited for clarity)
Thank you for your comment.
Doesn't it make the article more complicated to grasp ?
I just mentioned structural typing so the reader understand this is not necessarily a design mistake, and most typescripters already knows about structural typing or can easily learn about it from other sources.
I thought it would be nice to know upfront what's happening exactly and that this is not a bug in TypeScript, but I guess it could be overwhelming for a subset of the target audience. Thanks for the response.
You make some good points here, but I think it's incorrect to conflate the
in
operator with arbitrary type casting. To use a contrived example, TypeScript will accept this code, but it will "crash" your JavaScript application:This would be the "equivalent" code with an
in
check, but TypeScript will not accept it as valid:You would need to add an additional runtime check for TypeScript to be happy with it:
Now, that is still misleading code β the block will never be entered, because it's impossible for
foo
to have auserName
property β but that's really not the same as causing your application to crash.The fact is that JavaScript is a dynamically typed language, and sometimes in the real world we will encounter keys on objects that our types haven't fully accounted for (especially when depending on third-party JavaScript modules). But TS type annotations and runtime checks like
if (<key> in <object>) <...>
are fundamentally different things β TypeScript provides us some compile-time safety (and a nice developer experience), but as a static analysis tool, it's possible for its assumptions to be incorrect. It's not possible for anif (...)
block with a falsey condition to be entered at runtime, so the only thing TypeScript is doing here is updating its assumptions to reflect the reality that the runtime check has established.What you change the function signature from
function isSuperUser(user: User){
return "isSuperUser" in user;
}
to
function isSuperUser(user: User): user is SuperUser {
return "isSuperUser" in user;
}
Enabling explicit return types, exhaustive unit tests, and code reviews are the winners in this case, especially a typo too.
Even without exploring return types, if you wrote 100% test coverage there's a logical branch in the code which returns void that you would have to test. If you write the test covering that scenario to get 100% coverage, then you've either realized your code doesn't do what you want, or you've now documented that it's expected behavior.
The answer here is unit tests. TypeScript is covering a lot of cases, but not all.
Haven't heard of the structural type reference, learned something useful today!
btw. isn't the whole point for the code that is guarded by the
in
operator not to get executed?And it isnβt. OP is making the point that a typo can mess up your intention and produces unreachable code.