TS Exploring - Object vs object, Number vs number
- typescript
- javascript
1. Why can we use object but not Object?
When using TypeScript, there are times we utilize the types of built-in objects. For example:
const date: Date = new Date();
Additionally, due to the availability of the []
notation, it is a matter of personal preference, but we can also use the built-in object type Array
.
const arr: Array<number> = [1, 2, 3];
It then follows that there are similar types for Number
and String
. Indeed, such types exist. However, as you may know when first learning TS, the primitive type representing numbers is number
.
There is also the Object
type, but we are advised to use object
instead. Other primitive types we need to use are also written in lowercase. What is the difference?
2. Object
In JS, everything is an object. All objects ultimately have Object
constructor's Object.prototype
as their prototype, and for this reason, we can utilize the methods defined in Object.prototype
. Thus, we can view all objects as extensions of Object
.
The Object
type signifies all objects created through any constructor function in the prototype chain of the Object
constructor. All constructor functions in JS inherit from Object
. Therefore, due to TypeScript's type characteristics, Object
can include all JS objects except for null and undefined. Functions are also possible.
const foo: Object = (a: number) => a + 1;
2.1. Definition of Object type
How is the Object
type defined? The Object
type is defined in node_modules/typescript/lib/lib.es5.d.ts
as follows, containing all methods that objects created through the Object
constructor must have.
interface Object {
/** The initial value of Object.prototype.constructor is the standard built-in Object constructor. */
constructor: Function;
/** Returns a string representation of an object. */
toString(): string;
/** Returns a date converted to a string using the current locale. */
toLocaleString(): string;
/** Returns the primitive value of the specified object. */
valueOf(): Object;
/**
* Determines whether an object has a specified property name.
* @param v A property name.
*/
hasOwnProperty(v: PropertyKey): boolean;
/**
* Determines whether an object exists in another object's prototype chain.
* @param v Another object whose prototype chain is to be checked.
*/
isPrototypeOf(v: Object): boolean;
/**
* Determines whether a specified property is enumerable.
* @param v A property name.
*/
propertyIsEnumerable(v: PropertyKey): boolean;
}
2.2. Object Constructor type
However, there are also statically defined methods directly on the Object
constructor function, such as Object.keys()
. These are defined in the interface named ObjectConstructor
, which creates an Object
type when called with new
.
We also see that Object
is defined in relation to Object.prototype
. This is necessary for objects created using this constructor to have Object
as their prototype. If you need knowledge about prototype inheritance, it is covered in JS Exploration - Prototype Syntax.
interface ObjectConstructor {
new(value?: any): Object;
(): any;
(value: any): any;
/** A reference to the prototype for a class of objects. */
readonly prototype: Object;
/* Other methods omitted for brevity */
/**
* Returns the names of the enumerable string properties and methods of an object.
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
keys(o: object): string[];
}
3. Wrapper Object Types
What then are types like Number
and String
? They represent all objects created through the respective constructor (used as wrapper objects). The constructor functions are defined as types like NumberConstructor
.
3.1. Wrapper Object
Let's briefly return to JS. While programming in JS, one may encounter methods or properties that do not inherently belong to primitive values but can still be used, such as the string "hello"
having properties like length
or methods like indexOf
. Where do these come from?
This is made possible by wrapper objects. When attempting to access properties or methods on primitives or literals, a wrapper object is created by calling the constructor of the relevant object. This wrapper object possesses the properties or methods of the original object, hence it can be utilized.
For example, in the following code, when accessing str.length
, JS invokes new String(str)
to create a wrapper object. It then retrieves the string length of 5 from this wrapper object.
const str = "hello";
console.log(str.length); // 5
There are five types of such wrapper objects: String
, Number
, BigInt
, Boolean
, and Symbol
. Each is an object created through constructors like new String(~~)
.
3.2. Issues with Wrapper Object Types
Types like Number
and String
are all types of wrapper objects. More specifically, they represent the types of all objects created with the respective constructors. For instance, the Number
type represents all numerical wrapper objects created using new Number(number)
.
So why should we avoid using these wrapper object types? The issue arises because when we generally use primitives through variables, we want to access the data of the primitive value, not the wrapper object itself.
If we specify the type as these wrapper object types, we will face issues with even basic operations. For example, both are types of the number wrapper, and operations like addition do not exist between wrapper objects.
// Operator '+' cannot be applied to types 'Number' and 'Number'.
const a: Number = 3, b: Number = 4;
console.log(a + b);
However, methods that exist on the Number
wrapper object, such as toString
, can be used without issue.
const a: Number = 3;
console.log(a.toString());
Yet, in almost all cases where we are using a variable that is assigned a primitive value, there is virtually no need for the wrapper object. Even if there were, the wrapper object would be created as needed if the variable is defined with the primitive type. For these reasons, it is not advisable to use wrapper object types that impose limitations while removing necessary functionality.
4. Viewing Wrapper Objects
Typically, you can check the TypeScript type definition files located in node_modules
to see these types directly and confirm that types like Number
encompass objects created through their constructor.
4.1. Type Definition
For example, the Number
type is defined in node_modules/typescript/lib/lib.es2020.number.d.ts
as follows:
interface Number {
/**
* Converts a number to a string by using the current or specified locale.
* @param locales A locale string, array of locale strings, Intl.Locale object, or array of Intl.Locale objects that contain one or more language or locale tags. If you include more than one locale string, list them in descending order of priority so that the first entry is the preferred locale. If you omit this parameter, the default locale of the JavaScript runtime is used.
* @param options An object that contains one or more properties that specify comparison options.
*/
toLocaleString(locales?: Intl.LocalesArgument, options?: Intl.NumberFormatOptions): string;
}
Other methods related to numbers, like toFixed
, are also defined at the same location in lib.es5.d.ts
.
In the case of strings, there are numerous changes and features across versions, resulting in definitions being spread across more files. In lib.es5.d.ts
, many familiar string methods are defined as types. In fact, there are quite a few, but most of the string methods we know are defined.
interface String {
/** Returns a string representation of a string. */
toString(): string;
/* (omitted) */
/**
* Returns the position of the first occurrence of a substring.
* @param searchString The substring to search for in the string
* @param position The index at which to begin searching the String object. If omitted, search starts at the beginning of the string.
*/
indexOf(searchString: string, position?: number): number;
/* omitted */
/** Returns the length of a String object. */
readonly length: number;
/* omitted */
readonly [index: number]: string;
}
Furthermore, properties related to the string wrapper object are defined across files encompassing ES2015 changes, such as lib.es2015.core.d.ts
, lib.es2015.iterable.d.ts
, and well-known symbols defined in lib.es2015.symbol.wellknown.d.ts
.
4.2. Constructor Function Type
The types of constructor functions are also defined in lib.es5.d.ts
. For instance, the NumberConstructor
is defined as follows. Similar to the ObjectConstructor
above, it returns Number
when called with new
, and you can verify static properties like MAX_VALUE
contained in the Number
constructor (not the type).
It is personally interesting to note that NaN
is defined as a property here.
interface NumberConstructor {
new(value?: any): Number;
(value?: any): any;
readonly prototype: Number;
/** The largest number that can be represented in JavaScript. Equal to approximately 1.79E+308. */
readonly MAX_VALUE: number;
/** The closest number to zero that can be represented in JavaScript. Equal to approximately 5.00E-324. */
readonly MIN_VALUE: number;
/**
* A value that is not a number.
* In equality comparisons, NaN does not equal any value, including itself. To test whether a value is equivalent to NaN, use the isNaN function.
*/
readonly NaN: number;
/* (omitted) */
}
5. Conclusion
Types of built-in objects that start with uppercase letters, such as Number
and Object
, encompass all objects created with their respective constructors.
However, primitive values like Number
are rarely used for their wrapper objects, and Object
can encompass too many types because all object constructors originally inherit from the Object
constructor. Therefore, we prefer to use primitive types like number
or types that only encompass objects, like object
, according to our intended purposes.
5.1. Other Built-in Object Types
Other built-in objects such as Array
or Date
, which we observed previously, are objects by nature and are utilized for object purposes; therefore, it is acceptable to use the types they generate. This is why there is no separate type like array
.
Even when you look in lib.es5.d.ts
, Array
is defined as a type that encompasses objects created by the constructor function ArrayConstructor
, and Date
encompasses objects created by DateConstructor
. However, this poses no issue in this context, as array objects created with new Array
serve as objects in and of themselves, rather than as wrappers.