Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Accept any object as argument in function

Tags:

typescript

What I have

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.

What I want

I don't understand how to accept any object as an argument without having this kind of errors without the need to use any.

like image 201
Cristian Flórez Avatar asked Jan 24 '23 10:01

Cristian Flórez


1 Answers

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

like image 90
jcalz Avatar answered Jan 27 '23 00:01

jcalz