I have a function defined like this:
export async function removeUser(deleteUserId: Types.ObjectId)
When I mistakenly called this function and pass a Mongoose object's id
parameter, which is a string, it triggered a runtime exception further in the code when I tried to use the .equals()
method which exists on an ObjectId but not on a string:
await removeUser(deleteUser.id);
When I corrected this by passing the ObjectId, everything worked fine:
await removeUser(deleteUser._id);
My question is, why didn't I get a compile time error from Typescript if the argument is specified as an ObjectId, but a string is being passed?
Edit: I see that the .id
is defined as any
, not as a string
. I suspect that will factor into the answer?
CodePudding user response:
Yep, if we can somehow check for any, then we can make sure that values of type any
cannot be passed:
type IfAny<T, Y, N> = 0 extends (1 & T) ? Y : N;
export async function removeUser<Id extends Types.ObjectId>(deleteUserId: IfAny<Id, never, Id>) {
So if we pass in any
, the type of deleteUserId
will turn into never
:
await removeUser(deleteUser.id); // error, type 'any' is not assignable to type 'never'
await removeUser(deleteUser._id); // ok
CodePudding user response:
I see that the .id is defined as any, not as a string.
Values of type any
allow assignment to any type.
const n: number = 'string' as any // fine
It's an escape hatch from the type system. So any
is not typesafe and should be avoided at all costs.
The compiler effectively treats any as “please turn off type checking for this thing”. It is similar to putting an @ts-ignore comment around every usage of the variable.
CodePudding user response:
Typescript types exist only in compile time, not in runtime. Therefore, Typescript type system is not sound -- it cannot guarantee you that if your program compiles, it will not throw any error in runtime.
Back to your question, if a variable type is any
, you can do anything with it, and Typescript will not even blink. You should use any
type with an extreme caution.
For example, if you have a variable obj
of type any
:
- You could assign
obj
to any other variable (except of typenever
) - You could access any fields on
obj
e.g.obj.a.b.c.d
- You could pass it to any function, in particular that expecting
string
function onlyAcceptingNumber(a: number) {}
export async function doSomething(obj: any){
// I can do this:
const a = obj.a * obj.b;
// I can also do this:
console.log(obj.a.b.c.d);
// and I can call onlyAcceptingNumber with obj
onlyAcceptingNumber(obj);
}
// but of course this could fail in runtime:
doSomething("123");
doSomething({ name: "John" });
In the example above, the doSomething
function would compile, and both calls to it would compile, but of course if we run everything, it would fail in runtime.
To battle this, you can use e.g. ESLint's no-explicit-any rule, and in general only use any
as a last resort