I have a method in a class which I want to accept any object as an argument but I don't want to use any
, for this I use a generic
and extend object
.
class MyClass {
saveObject<T extends object>(object: T | null) {}
}
With this implementation, @typescript-eslint/ban-types
rule complains with the following error:
Don't use 'object' as a type. The 'object' type is currently hard to use see this issue(https://github.com/microsoft/TypeScript/issues/21732)). Consider using 'Record<string, unknown>' instead, as it allows you to more easily inspect and use the keys.
ok, then I will listen to Eslint and do the following implementation:
class MyClass {
saveObject<T extends Record<string, unknown>>(object: T | null) {}
}
with the above implementation, the Eslint error disappears, so I try to execute the method with a random object:
anyOtherMethodInMyCode(payment: IPaymentModel | null): void {
// execute our method
this.saveObject(payment);
}
But the typescript compiler throws a new error:
TS2345: Argument of type 'IPaymentModel | null' is not assignable to parameter of type 'Record<string, unknown> | null'.
Type 'IPaymentModel' is not assignable to type 'Record<string, unknown>'.
Index signature is missing in type 'IPaymentModel'.
One option is to use Type Assertion
in the argument passed to the method as follow:
anyOtherMethodInMyCode(payment: IPaymentModel | null): void {
// execute our method with Type Assertion
this.saveObject(payment as Record<string, unknown>);
}
With the above, TS compiler errors disappear but it is not optimal to have to do this(Type Assertion) in all places where the method is executed.
I don't understand how to accept any object as an argument without having this kind of errors without the need to use any
.
I don't completely agree with the premise of the default configuration of
typescript-eslint's ban-types
rule which says that
Avoid the
object
type, as it is currently hard to use due to not being able to assert that keys exist. See microsoft/TypeScript#21732.
As the person who filed the linked issue, I do understand that it is painful to try to use built-in type guarding to take a value of type object
and do anything useful with its properties. It would be great if this were fixed. However, the type object
represents "a non-primitive value in TypeScript" in a way that Record<string, unknown>
does not. And, as you've noticed, Record<string, unknown>
has its own problems, such as microsoft/TypeScript#15300. TypeScript has lots of pitfalls and pain points, and a blanket recommendation against one in favor of another doesn't seem advisable to me.
--
For this particular use case, you can switch from object
to {[k: string]: any}
and not {[k: string]: unknown}
. If you make this change, it will be easier to process the value inside of saveObject
:
saveObject(obj: Record<string, any> | null) {
if (obj === null) {
console.log("nothing to save");
return;
}
if (obj.format === "json") {
// do something
}
}
(I've changed your example so that it is not generic; this may be important for your actual use case, but as a code example there's no point in making a function generic if that generic-ness isn't used anywhere. A function with type signature <T extends U>(x: T)=>void
can very often be replaced with (x: U)=>void
with no ill effects)
This increased ease of use is not really type safe, since having properties of type any
are similar to turning off type checking. But there is special casing in TypeScript which will allow any object to be assignable to {[k: string]: any}
but not {[k: string]: unknown}
. The latter type prohibits any interface
types without an explicit index signature (see microsoft/TypeScript#15300), while the former is very similar to object
and does not have this restriction (see microsoft/TypeScript#41746).
If that works for you (and you are not using linting to prohibit any
), then you will find things working better:
anyOtherMethodInMyCode(payment: IPaymentModel | null): void {
this.saveObject(payment); // okay
}
otherTests(): void {
this.saveObject("not an object"); // error!
this.saveObject(() => 3); // okay, a function is an object
}
I would still say that unless object
gives you some specific problem inside the implementation of saveObject()
, it is reasonable to disable the linter for that one line and use object
instead. It is more expressive of "any non-primitive" than Record
is. According to the linter, the supposed reason not to use object
is that it is hard to use. That is true; but it's easy to supply, and if you would like to call this.saveObject()
in more places than you would like to implement, I'd rather do one annoying thing inside the implementation and not many annoying things at each call site.
Playground link to code
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With