Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript generics - is "extends object" pointless? What is the best practice?

I've noticed that <P extends object> generic is usually pointless because basically everything in javascript is an object. Most literals are objects with .toString method. A string is an object with a .length property, etc. I've come to prefer just <P> but curious what others have noticed.

I don't have a good example right now, I'm more just trying to hear about other people's experience.

like image 355
Devin Rhode Avatar asked Jan 28 '21 21:01

Devin Rhode


People also ask

Why do we use generics in TypeScript?

Generics allow creating 'type variables' which can be used to create classes, functions & type aliases that don't need to explicitly define the types that they use. Generics makes it easier to write reusable code.

What encloses a generic in TypeScript?

A generic class has a type parameter enclosed in angle brackets (< >) immediately after the class name. In generic class, the same type parameter can be used to annotate method parameters, properties, return types, and local variables.

Does TypeScript support generics?

TypeScript fully supports generics as a way to introduce type-safety into components that accept arguments and return values whose type will be indeterminate until they are consumed later in your code.

Do TypeScript generics work with primitives?

Using generics allows us to write a typesafe code that will work with a wide range of primitives and objects.


1 Answers

See "The object Type in TypeScript" for more information.

The object type in TypeScript was introduced specifically to exclude the seven primitive types, string, number, boolean, bigint, symbol, undefined, and null. (Yes, typeof null === "object" at runtime, but it is still considered primitive in JS and TS). It is true that string, number, boolean, bigint, and symbol values will be automatically wrapped in String, Number, Boolean, BigInt, and Symbol objects (respectively) when you access members on them as if they were objects. But they are distinguishable from true objects, and sometimes this makes a difference. The example given in the TypeScript Handbook is the Object.create(), which leads to runtime errors if passed an argument of a primitive type (except for null). Hence TypeScript's typing for Object.create() specifies that its argument is of type object | null. If you want your generic parameter to exclude primitives, <P extends object> would be the right way to do it... so it isn't pointless.

Note that there is also an Object interface in TypeScript, starting with an uppercase O. This interface contains the (apparent) members that exist on everything in JS, like valueOf() and toString(). It might be closer to what you were thinking of when you said "everything is an object"; only null and undefined are not assignable to Object. Generally speaking, though, you probably don't want to use the Object type in TypeScript; such wrapper types are hardly ever what people want to use.

If you really want to capture "anything which can be indexed into like an object", you should probably use the so-called "empty object" type, {}. This is an object type with no known properties, and behaves like Object. Again, only null and undefined are not assignable to {}. In fact, it used to be the case that unconstrained generic type parameters (like <P> instead of <P extends Q>) were implicitly constrained to {}. So it used to be quite literally useless to write <P extends {}>.

Since TypeScript 3.5, however, unconstrained generics are now given an implicit constraint of unknown instead of {}. The unknown type really is "everything" in TypeScript. You can assign any value whatsoever to a variable of type unknown (but not vice-versa). It is truly pointless to write <P extends unknown>.

And we might as well end with any, the ultimate "anything-goes" type. Not only can you assign anything to any (like unknown), you can also assign any to anything (like never). Using any is like throwing up your hands and giving up; it's more of a disabling of type checking than it is an actual type. Since TypeScript 3.9, writing <P extends any> is the same as writing <P extends unknown>, and therefore, similarly pointless. (It used to be that <P extends any> allowed you to treat P like any when P was unresolved, like in a generic function implementation, but that was considered silly and changed.)

Playground link to code

like image 188
jcalz Avatar answered Oct 24 '22 04:10

jcalz