Accept any object as argument in function

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

Leave a Comment