Categories
JavaScript Nodejs

Node.js’ fs Module — Getting File Information

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, being able to manipulate files is a basic feature.

Fortunately, Node.js has an fs module built into its library. It has many functions that can help with manipulating files and folders. File and directory operations that are supported include basic ones like manipulating and opening files in directories.

Likewise, it can do the same for files. It can do this both synchronously and asynchronously. It has an asynchronous API that has functions that support promises. Also, it can show statistics for a file.

Almost all the file operations that we can think of can be done with the built-in fs module.

In this article, we will use the functions in the fs module to get data about a file located in the fs.Stats object with the fs.stat(), fs.lstat(), and fs.fstat() functions.

To get information about a file, we can use the fs.Stats object, which is returned by the fs.stat(), fs.lstat(), and fs.fstat() functions, and their synchronous counterparts.

They can display numeric data as bigint as it’s passed in as an option key with its value set to true. It has nanosecond-precision properties suffixed with Ns.

The stat function takes a path object which can be a string, Buffer, or a URL object as the first argument.

A second argument is an object that can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

The stat function runs asynchronously. Its synchronous counterpart is the statSync function, which takes the same first two arguments without the callback function. statSync returns the file information as an object.

lstat is similar to stat, but it doesn’t follow the symbolic link. It takes a path object which can be a string, Buffer, or a URL object as the first argument.

A second argument is an object which can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

When the path that’s passed in is a symbolic link, then it gives the information about the symbolic link. lstat runs asynchronously, so that the data is retrieved in an indeterminate amount of time.

Its synchronous counterpart, the lstatSync function, takes the same arguments as the lstat function without the callback function and returns the Stat object which has the file information.

The fstat function is similar to the stat function. It takes a path object which can be a string, Buffer, or an URL object as the first argument.

The second argument is an object which can take the bigint as the key, which is a boolean value. If it’s set to true, then numerical information will be returned as bigInts.

The third argument is a callback function that has the error object for the first parameter and the stats object as the second parameter, which has the information about a file and it’s running when the file information is retrieved.

The only difference between stat and fstat is that it takes a file descriptor instead of a path object.

We can get the file descriptor from the callback that’s accepted by the fs.open function and its promise and synchronous counterparts, fsPromises.open, and fs.opensync.

To use the fs.stat function, we can use it like in the following code:

const fs = require("fs");
fs.stat("./files/file.txt", (err, stat) => {  
  if (err) throw err;  
  console.log(stat);  
});

Then, if we run the code above, we get something like the following output:

Stats {  
  dev: 3605029386,  
  mode: 33206,  
  nlink: 1,  
  uid: 0,  
  gid: 0,  
  rdev: 0,  
  blksize: 4096,  
  ino: 22799473115106240,  
  size: 0,  
  blocks: 0,  
  atimeMs: 1572569358035.625,  
  mtimeMs: 1572569358035.625,  
  ctimeMs: 1572569358035.625,  
  birthtimeMs: 1572569358035.625,  
  atime: 2019-11-01T00:49:18.036Z,  
  mtime: 2019-11-01T00:49:18.036Z,  
  ctime: 2019-11-01T00:49:18.036Z,  
  birthtime: 2019-11-01T00:49:18.036Z  
}

As we can see, the Stats object has many properties. The data properties are listed above. It also has a few function properties.

The data properties in the Stats object means the following:

  • dev — The numeric identifier of the device storing the given file. It can be a number or a bigInt.
  • ino — The “inode” number of the file. It’s a number that contains basic information about a file, directory, or other file system object. It can be a number or a bigInt.
  • mode — Bit-field description of the file type and mode. It can be a number or a bigInt.
  • nlink — Number of hard links that exist for the file. It can be a number or a bigInt.
  • uid — The numeric user identifier of the user that owns the file. Applicable to POSIX systems only. It can be a number or a bigInt.
  • gid — The numeric group identifier of the user that owns the file. Applicable to POSIX systems only. It can be a number or a bigInt.
  • rdev — Numeric device identifier of the file if it’s a special file. A file is special if it’s used for I/O. For example, page files and hibernation files are considered special files. It can be a number or a bigInt.
  • size — The size of the file in bytes. It can be a number or a bigInt.
  • blksize — The block size for a file system I/O. It can be a number or a bigInt.
  • blocks — The number of blocks allocated to the file. It can be a number or a bigInt.
  • atimeNs — The timestamp indicating when the file was last accessed in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • mtimeNs — The timestamp indicating when the file was last modified in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • ctimeNs — The timestamp indicating when the file was last changed in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • birthtimeNs — The timestamp indicating when the file was created in nanoseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • atime — The timestamp indicating when the file was last accessed in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • mtime — The timestamp indicating when the file was last modified in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • ctime — The timestamp indicating when the file was last changed in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.
  • birthtime — The timestamp indicating when the file was created in milliseconds since the POSIX Epoch, which is the time relative to January 1, 1970 midnight. It can be a number or a bigInt.

The Stats object also has the following function properties to check for the basic information about a file:

  • isBlockDevice() — This is a function with a boolean return value that returns true if the file is a block device. A block device refers to a file that represents the device that stores files in blocks and also retrieves them as such.
  • isCharacterDevice() — This is a function with a boolean return value that returns true if the file is a character device. A character device refers to a file that represents the device that provided unbuffered, direct access to the hardware device. They don’t have to allow programs to read or write a single character at a time.
  • isDirectory() — This is a function with a boolean return value that returns true if the item is a directory.
  • isFIFO() — This is a function with a boolean return value that returns true if the item is a first-in-first-out pipe. FIFO pipe means that the first bits of a file going into the device will be the same ones that come out when retrieved. It only allows for unidirectional communication.
  • isFile() — This is a function with a boolean return value that returns true if the item is a file.
  • isSocket() — This is a function with a boolean return value that returns true if the item is a socket. A socket is a special file that enables communication between two processes. It can send data and file descriptors across a domain socket. It can do bidirectional communication.
  • isSymbolicLink() — This is a function with a boolean return value that returns true if the item is a symbolic link. A symbolic link is a reference to another file or directory in the form of an absolute or relative path.

To use the synchronous version of the stat function, the statSync function, we can write something like the following code:

const fs = require("fs");
const stat = fs.statSync("./files/file.txt");  
console.log(stat);

The Stat object is returned directly from the statSync function.

To use the fstat function, we have to get the file descriptor first, which we can do with the open function and its variants. For example, if we want to use the open function to get the file descriptor, we can write the following:

const fs = require("fs");
fs.open("./files/file.txt", "r", (err, fd) => {  
  if (err) throw err;  
  fs.fstat(fd, (err, stat) => {  
    if (err) throw err;  
    console.log(stat);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

We can use fstat with the promise version of the open function like in the following code:

const fsPromises = require("fs").promises;  
const fs = require("fs");
(async () => {  
  const fdObj = await fsPromises.open("./files/file.txt", "r");  
  fs.fstat(fdObj.fd, (err, stat) => {  
    if (err) throw err;  
    console.log(stat);  
    fs.close(fdObj.fd, err => {  
      if (err) throw err;  
    });  
  });  
})();

The promise version of the open function returns a promise that resolves to an object with the file descriptor inside it. We can use the fd property to get the file descriptor and pass it into the fstat function.

Likewise, with the lstat function, we can call it as in the following code:

const fs = require("fs");
fs.lstat("./files/file.txt", (err, stat) => {  
  if (err) throw err;  
  console.log(stat);  
});

The lstat does almost everything as stat does, except that it gets the data of a symbolic link instead of following it, so we will get output similar to the ones above.

The fs.stat(), fs.lstat(), and fs.fstat() functions are very useful for getting data about files, directories, and symbolic links.

They can get us the Stat object which has lots of information that we can use, including access time, modified time, whether the file is a special file, the device it’s stored on, and many other pieces of information through its value and function properties.

Data can be displayed as numbers or bigInt if they’re numerical and timestamps are available in both milliseconds and nanoseconds for extra precision.

Categories
JavaScript Nodejs

Node.js’s ‘fs’ Module: Writing Files and Directories

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, the ability to manipulate files is a basic feature.

Fortunately, Node.js has a fs module built into its library. It has many functions that can help with manipulating files and folders. It supports basic file and directory operations, like manipulating and opening files in directories.

It can do the same for files. It can do this both synchronously and asynchronously. It has an asynchronous API with functions that support promises. Also, it can show statistics for a file.

Almost all the file operations that we can think of can be done with the built-in fs module. In this piece, we’ll use the functions in the fs module to write files with the write family of functions to write files.


fs.Write

There are two versions of the write functions, one for writing text to disk and another for writing binary data to disk.

The text version of write function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor — a number that identifies the file.

The second argument is a string that’s written to the file. If the value passed in is not a string, it is converted to one.

The third argument is the position in which the file writing starts. If the value passed in isn’t a number, then it starts in the current position.

The fourth argument is a string that has the character encoding of the file to be written, which defaults to be utf8. The last argument is a callback function with three parameters.

The first is the err object which has the error object and it’s not null if there’s an error.

The second parameter is the written parameter, an integer which specifies how many bytes are written to the file system. It’s not necessarily the same as the number of string characters written.

The third parameter is the string parameter that has the string that was written.

On Linux, positional writes don’t work in the append model. On Windows, if the file descriptor is 1, which stands for the standard output, then strings that have non-ASCII characters won’t be rendered properly by default.

To use the write function, we can use the open function to get the file descriptor of the file you want to write to first, then we can write to the file by passing in the file descriptor to the write function. For example, if we want to write to the file with the path ./files/file.txt , we can write something like this:

const fs = require("fs");
fs.open("./files/file.txt", "r+", (err, fd) => {  
  if (err) throw err;  
  fs.write(fd, "abc", 0, "utf8", (err, written, string) => {  
    console.log(err, written, string);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

When we run the code above, we should get output that looks something like this:

null 3 abc

In the code above, we first open the file with the open function. We pass in the r+ flag so that we can write to the file. Then we get the file descriptor fd in the callback function that we passed into the open function. With the fd file descriptor, we can pass it into the write function.

In the second argument of the write function we specified that we want to write the string abc to the file. In the third argument, we specified that we want to write it at position 0, the fourth argument specifies that the character encoding of the string should be UTF-8.

The callback in the last argument would get us the result of the write. From there, we know from the output that three bytes and the string ‘abc’ were written to the file.

Other than the r+ flag, there are many other possible system flags, including:

  • 'a' — Opens a file for appending, which means adding data to the existing file. The file is created if it does not exist.
  • 'ax' — Like 'a' but an exception is thrown if the path exists.
  • 'a+' — Open file for reading and appending. The file is created if it doesn’t exist.
  • 'ax+' — Like 'a+' but an exception is thrown if the path exists.
  • 'as' — Opens a file for appending in synchronous mode. The file is created if it does not exist.
  • 'as+' — Opens a file for reading and appending in synchronous mode. The file is created if it does not exist.
  • 'r' — Opens a file for reading. An exception is thrown if the file doesn’t exist.
  • 'r+' — Opens a file for reading and writing. An exception is thrown if the file doesn’t exist.
  • 'rs+' — Opens a file for reading and writing in synchronous mode.
  • 'w' — Opens a file for writing. The file is created (if it does not exist) or overwritten (if it exists).
  • 'wx' — Like 'w' but fails if the path exists.
  • 'w+' — Opens a file for reading and writing. The file is created (if it does not exist) or overwritten (if it exists).
  • 'wx+' — Like 'w+' but an exception is thrown if the path exists.

The binary version of the write function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor which is a number that identifies the file.

The second argument is the buffer object which can be of type Butter, TypedArray or DataView.

The third argument is the offset , which determines the part of the buffer to be written.

The fourth argument is the length argument that specifies the number of bytes being written, the last argument is the position which is an integer which describes the position in which the write function will start writing.

The final argument is a callback function — a function that takes the err parameter, which has the error object. If an error occurs, the second is the bytesWritten parameter which gets us the number of bytes written to disk, the third is the buffer object, which has the binary data which was written to disk.

For example, we can use it as in the following code:

const fs = require("fs");
fs.open("./files/binaryFile", "w", (err, fd) => {  
  if (err) throw err;  
  fs.write(fd, new Int8Array(8), 0, 8, 0, (err, bytesWritten, buffer) => {  
    console.log(err, bytesWritten, buffer);  
    fs.close(fd, err => {  
      if (err) throw err;  
    });  
  });  
});

We get the following output if we run it:

null 8 Int8Array [  
  0, 0, 0, 0,  
  0, 0, 0, 0  
]

fs.writeSync

The synchronous version of the write function is the writeSync function. There’s one version for writing binary data and one for writing text data.

The text version of thewriteSync function lets us write text onto the disk asynchronously. It takes a few arguments.

The first argument is the file descriptor which is a number that identifies the file.

The second argument is a string that will be written to the file. If the value passed in is not a string, it will be converted to one.

The third argument is the position in which the file writing starts. If the value passed in isn’t a number, then it starts in the current position.

The fourth argument is a string that has the character encoding of the file be written, which defaults to be utf8. The number of bytes written is returned.

We can use the text version of writeSync function as in the following code:

const fs = require("fs");
const fd = fs.openSync("./files/file.txt", "r+");  
const numBytesWritten = fs.writeSync(fd, "abc", 0, "utf8");  
console.log(numBytesWritten);

We should get 3 in the console.log statement since 3 bytes were written.

The binary data version of the writeSync function takes five arguments. It takes a file descriptor as the first argument. The second argument is the buffer object, which can be a Buffer, TypedArray or DataView object.

The third argument is the offset, which is the integer which specifies the part of the buffer to which the writeSync function will start writing.

The fourth argument is the length argument, which specifies the number of bytes being written.

The last argument is the position, which is an integer that describes the position in which the writeSync function will start writing. The number of bytes written is returned.

We can use the binary version of the writeSync function as in the following code:

const fs = require("fs");
const fd = fs.openSync("./files/binaryFile", "w");  
const numBytesWritten = fs.writeSync(fd, new Int8Array(8), 0, 8, 0);  
console.log(numBytesWritten);

We should get 8 in the console.log statement since 8 bytes were written.


fs.writeFile

We can write files in a less complicated way with the fs.writeFile function. It takes four arguments.

The first argument is the reference to the file, which can be a string path, a Buffer object, a URL object or a file descriptor, which is an integer that identifies the file.

The second argument is the data to be written to the file, which can be a string, a Buffer object, a TypedArray, or a DataView object. The third arguments is for passing in an object with a few options.

There are three things that can be set: the encoding, the mode, and the flag:

  • The encoding specifies the character encoding of the text being written which defaults to utf8 .
  • The mode is an integer which defaults to 0o666. The mode sets the file permission and the sticky bits but only if the file was already created. 0o666 means both readable and writable.
  • The flag option specifies the read or writes options of the file being written. The ones listed above are all valid for this argument. The last argument is the callback function, which is a function that takes an err object which is not null if an error exists.

For example, if we want to write to the file with the path ./files/file.txt with the writeFile function, we can write something like this:

const fs = require("fs");
fs.writeFile(  
  "./files/file.txt",  
  "abc",  
  { encoding: "utf8", mode: 666, flag: "w" },  
  err => console.log(err)  
);

In the first argument, we pass in the string path of the file. Then, in the second argument, we passed in the content to be written. The third argument has the encoding options, file permission of the file being written with the mode option, and the flag to set the mode that the file is open with. If the file is written successfully. The console.log statement above should output null and that you can see the content of the file you wrote onto the disk.

When a file descriptor is passed into the first argument of the writeFile function, the behavior is slightly different from passing in other objects to reference the file. The file is not replaced if a file descriptor is passed into the first argument.

This means that if the writeFile function is called multiple times with the file descriptor, then the item that’s written to the file will be appended to the file instead of overwriting the existing content. So if the first writeFile call passes in the file descriptor 1 and content a and the second writeFile call passes in the file descriptor 1 and content b, then we get ab in the file with file descriptor 1.


fs.writeFileSync

There’s a synchronous version of writeFile called writeFileSync, which takes the same arguments as the writeFile function except without the callback. It returns undefined . We can use it as in the following function:

const fs = require("fs");
fs.writeFileSync("./files/file.txt", "abc", {  
  encoding: "utf8",  
  mode: 666,  
  flag: "w"  
});

After running the code above, you should see the content written to disk in the file with the given path.

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, being able to manipulate files is a basic feature. Fortunately, Node.js has a fs module built into its library.

It has many functions that can help with manipulating files and folders. In this piece, we used the functions in the fs module to write files with the write family of functions to write files. We used the open and write functions to first open the file to get the file descriptor, then to write the content to the file. It has separate versions to work with text and binary files.

There’s also a synchronous version of the function called writeSync which takes the same arguments without the callbacks. It also has a text and binary version. To make writing to files more convenient, we can use the writeFile and the writeFileSync function, which can write both text and binary files given reference to a file like a path string or a URL object. writeFile is asynchronous and writeFileSync is synchronous.

Categories
JavaScript TypeScript

TypeScript Data Types – Null, Void, Undefined, Never and Object

JavaScript, like any other programming language, has its own data structures and types.

JavaScript has a few data types that we have to know, to build programs with it. Different pieces of data can be put together to build more complex data structures.

JavaScript is a loosely typed, or dynamically typed, language. This means that a variable that’s declared with one type can be converted to another type without explicitly converting the data to another type.

Variables can also contain any type at any time, depending on what’s assigned. With dynamically typed languages, it’s hard to determine the type that a variable has without logging it and we might assign data that we don’t want in the variable.

TypeScript rectifies these issues by letting us set fixed types for variables so that we’re sure of the types. In this article, we’ll look at the void, null, undefined, never, and the object types.


Void

The void type is pretty much the opposite of the any type. It means the absence of any type. So, the variable of the void type can only have the value null if the --strictNullChecks setting isn’t specified when running the TypeScrip compiler or it can be set to undefined.

Therefore, directly assigning values to a void variable isn’t very useful. It’s more used for specifying the return data type of a function. A function that has the void return type doesn’t return anything.

For example, we can do a useless assignment to a void variable like in the following code:

let useless: void = undefined;

We can set the return type of an arrow function to void by writing:

const voidFn = (): void => {  
  console.log("Void function returns nothing");  
}
voidFn();

Alternatively, we can set the return type to void for a traditional function like in the following code:

function voidFn(): void {  
  console.log("Void function returns nothing");  
}
voidFn();

Both function calls will output Void function returns nothing from the console.log statement inside the function.


Null

The null type represents a variable that can only take on the value null. null means that there’s no value for a variable.

So, once again, assigning a value to it is pretty useless. We can only assign null to a variable that has the variable null. With the --strictNullChecks flag set when running the TypeScript compiler, null can only be assignable to variables with the any type and the null type.

Where it does become useful is that we can have variables that can have values from more than one assigned to it with union types.

Then, we can specify something useful along with the null type. This is different from JavaScript in that the value null is the type object instead of null for historical reasons.


Undefined

The undefined type represents a variable that can only take on the value undefined. So, once again, assigning a value to it is pretty useless.

We can only assign undefined to a variable that has the variable null. undefined can only be assignable to variables with the any type and the undefined type.

Where it does become useful is that we can have variables that can have values from more than one assigned to it with union types.

Then, we can specify something useful along with the undefined type. It’s practically useless on its own, so we shouldn’t see many cases with variables that have only the undefined type.


Never

The never type is a type of value that represents something that never occurs. It’s like void in that it’s useful for designating that a function never returns anything.

The never type is a sub-type of, and is assignable to, every type. However, no type is a sub-type of, or assignable to, the never type except for other never variables.

A function that has a never return type must always have an unreachable endpoint. For example, we can write a function that has an infinite loop that has the never return type like in the following code:

function infiniteFn(): never {  
  while (true) {  
  }  
}

A function that throws an exception may also have the never return type, like in the following example:

function errorFn(message: string): never {  
  throw new Error(message);  
}
errorFn('Error occurred');

Object

The object type is a type that represents non-primitive objects. That is, anything that’s not a number, string, boolean, bigint, symbol, null, or undefined.

It’s mainly used in the type definition of the Object object in the standard library and other pieces of code that don’t want primitive values to be assigned to it, or passed into a function.

For example, in the type definition of the Object.create method, we see that the type of the parameter is set to the object like in the following code:

create(o: object | null): any;

Likewise, in the signature of the setPrototypeOf method in the same type declaration, we see that the proto parameter, which is the parameter that takes the prototype of an object, also has the object type set, as it does below:

setPrototypeOf(o: any, proto: object | null): any;

This way, we can’t pass in primitive values into these methods as arguments in TypeScript. If we did, then we would get an error. So, if we have the following code in our TypeScript files, then they would be compiled and run:

const obj1 = Object.create({});      
const obj2 = Object.create(null);  
console.log(obj1);  
console.log(obj2);

We would get an empty object in both console.log statements.

The only difference is that obj2 is a pure object, which means it doesn’t inherit from any prototype. Passing in primitive values to the create method like in the code below will cause compilation to fail:

Object.create(42);   
Object.create("abc");   
Object.create(false);  
Object.create(undefined)

The code above would get us an Argument of type ‘42’ is not assignable to parameter of type ‘object | null’ error for the first one, Argument of type ‘abc’ is not assignable to parameter of type ‘object | null’ for the second one.

The third line would get us Argument of type ‘false’ is not assignable to parameter of type ‘object | null’, and the last line would get us Argument of type ‘undefined’ is not assignable to parameter of type ‘object | null’.

Compilation would fail and the code wouldn’t run.

Also, we can use it to prevent primitive values from being assigned to it. For example, if we write:

let x: object = {};

Then, the code above would be compiled and run. However, if we write the following instead:

let x: object = 1;

Then we get Type ‘1’ is not assignable to type ‘object’ and the code can’t be compiled with the TypeScript compiler and be run, since 1 is a primitive value.

The void type is pretty much the opposite of the any type. It means the absence of any type. So, the variable of the void type can only have the value null if the --strictNullChecks setting isn’t specified when running the TypeScrip compiler or it can be set to undefined.

It’s useful for declaring functions that don’t return anything and not much else. The null type variable can only take on the null value. The undefined type can only be assigned the value undefined.

The object type is a type that represents non-primitive objects. That is, anything that’s not a number, string, boolean, bigint, symbol, null, or undefined.

It’s mainly used in the type definition of the Object object in the standard library and other pieces of code that don’t want primitive values to be assigned to it, or passed into a function.

Categories
JavaScript TypeScript

TypeScript Data Types – Numbers, Strings, and Objects

JavaScript, like any other programming language, has its own data structures and types. JavaScript has a few data types that we have to know about in order to build programs with it. Different pieces of data can be put together to build more complex data structures.

JavaScript is a loosely typed, or dynamically typed, language. This means that a variable that’s declared with one type can be converted to another type without explicitly converting the data to another type. Variables can also contain any type at any time, depending on what’s assigned.

With dynamically typed languages, it’s hard to determine the type that a variable has without logging it, and we might assign data that we don’t want in the variable.

TypeScript rectifies these issues by letting us set fixed types for variables so that we’re sure of the types. It has all the basic data types of JavaScript plus the types that are exclusive to TypeScript, like numbers, strings, and objects.


Numbers

There are two number types in JavaScript, which are number and BigInt. The number type is a double-precision 64-bit number that can have values between -2 to the 53rd power minus 1 and 2 to the 53rd power minus 1. There’s no specific type for integers. All numbers are floating-point numbers. There are also three symbolic values: Infinity, -Infinity, and NaN.

The largest and smallest available values for a number are Infinity and -Infinity, respectively. We can also use the constants Number.MAX_VALUE or Number.MIN_VALUE to represent the largest and smallest numbers. We can use the Number.isSafeInteger() function to check whether a number is in the range of numbers available that are allowed in JavaScript.

There are also the constants Number.MAX_SAFE_INTEGER and Number.MIN_SAFE_NUMBER to check if the number you specify is in the safe range. Anything outside the range isn’t safe and will be a double-precision floating-point of the value. The number 0 has two representations in JavaScript: There’s +0 and -0, and 0 is an alias for +0. It will be noticed if you try to divide a number by 0:

1/+0 // Infinity  
1/-0 // -Infinity

Sometimes numbers can represent Boolean values with bitwise operators to operate them as Boolean, but this is bad practice since JavaScript already has Boolean types, so using numbers to represent Boolean will be unclear to people reading the code. That’s because numbers can represent numbers, or they can represent Booleans if someone chooses to use them that way.

We can declare numbers like in the following code with TypeScript:

const x: number = 1  
const y: number = x + 1;

In TypeScript, there is the BigInt type to store numbers that are beyond the safe integer range. A BigInt number can be created by adding an n character to the end of a number. With BigInt, we can make calculations that have results beyond the safe range of normal numbers. For example, we can write the following expressions and still get the numbers we expect:

const x: bigint = 2n ** 55n;  
const y: bigint = x + 1n;

For x we get 36028797018963968n and for y we get 36028797018963969n, which is what we expect. BigInts can use the same arithmetic operations as numbers, such as +, *, -, ** and %. A BigInt behaves like a number when converted to a Boolean, with functions, keywords, or operators like Boolean, if , || , &&, !. BigInts cannot be operated in the same expressions as numbers. If we try that, we will get a TypeError. This is enforced with TypeScript compiler checks before compilation is done.


Strings

Strings are used to represent textual data. Each element of the string has its own position in the string. It’s zero-indexed, so the position of the first character of a string is 0. The length property of the string has the total number of characters of the string.

JavaScript strings are immutable. We cannot modify a string that has been created, but we can still create a new string that contains the originally defined string. We can extract substrings from a string with the substr() function and use the concat() function to concatenate two strings.

We should only present text data with strings. If there are more complex structures needed for your data structure, then they shouldn’t be represented with a string. Instead, they should be objects. This is because it’s easy to make mistakes with strings since we can put in the characters we want.

We can declare strings in TypeScript with the following code:

const x: string = 'abc';

Objects

Object is a reference data type, which means it can be referenced by an identifier that points to the location of the object in memory. In memory, the object’s value is stored, and, with the identifier, we can access the value. Object has properties, which are key-value pairs with the values being able to contain the data with primitive types or other objects.

That means we can use object to build complex data structures. The key is an identifier for the values of a property, which can be stored as a string or symbol. There are two types of properties that have certain attributes in an object. Objects have data properties and accessor properties.

A JavaScript object has the following data properties:

  • [[Value]] — This can be of any type. It has the value retrieved by a getter of the property. Defaults to undefined.
  • [[Writable]] — This is a Boolean value. If it’s false, then [[Value]] can’t be changed. Defaults to false.
  • [[Enumerable]] — This is a Boolean value. If it’s true, then it can be iterated over by the for...in loop, which is used to iterate over the properties of an object. Defaults to false.
  • [[Configurable]] — This is a Boolean value. If it’s true, then the property can be deleted or changed to an accessor property, and all attributes can be changed. Otherwise, the property can’t be deleted or changed to an accessor property, and attributes other than [[Value]] and [[Writable]] can’t be changed. Defaults to false.

A JavaScript object has the following accessor properties:

  • [[Get]] — This is either a function or it’s undefined. This may contain a function that is used to retrieve a property value whenever a property is being retrieved. Defaults to undefined.
  • [[Set]] — This is either a function or it’s undefined . This lets us set the assigned value to an object’s property whenever an object’s property is attempted to be changed. Defaults to undefined.
  • [[Enumerable]] — This is a Boolean value that defaults to false. If it’s true, then the property will be included when we loop through the properties with the for...in loop.
  • [[Configurable]] — This is a Boolean value that defaults to false. If it’s false, then we can’t delete the property and can’t make changes to it.

JavaScript has a Date object built into the standard library, so we can use it to manipulate dates.

Array are also objects. Array can store a list of data with integer indexes to indicate its position. The first index of JavaScript arrays is 0. There’s also a length property to get the size of the array. The Array object has many convenient methods to manipulate arrays, like the push method to add items to the end of the array and the indexOf method to find the index of the first occurrence of a given value. ATypedArray object is an object that lets us see an array-like view of a binary data buffer.

Since ES2015, the following typed array objects are available:

  • Int8Array, value ranges from -128 to 127
  • Uint8Array, value ranges from 0 to 255
  • Uint8ClampedArray, value ranges from 0 to 255
  • Int16Array, value ranges from -32768 to 32767
  • Uint16Array, value ranges from 0 to 65535
  • Int32Array, value ranges from -2147483648 to 2147483647
  • Unit32Array, value ranges from 0 to 4294967295
  • Floar32Array, value ranges from -1.2 times 10 to the 38 to 3.4times 10 to the 38
  • Float64Array, value ranges from 5.0 times 10 to the 324 to 1.8 times 10 to the 308
  • BigInt64Array, value ranges from -2 to the 63 to 2 to the 63 minus 1
  • BigUint64Array, value ranges from 0 to 2 to the 64 minus 1

Since ES2015, we have new iterable object types. They are Map, Set, WeakMap, andWeakSet. Set and WeakSet represent sets of objects, and Map and WeakMap represent objects with a list of key-value pairs. Map keys can be iterated over, butWeakMap’s keys cannot be.

With TypeScript, we put constructor names after the colon in the variable declaration to declare their types. For example, if we want to declare a Map object, we can write:

const map: Map<string, number> = new Map([['a', 1], ['b', 2]]);

Note that the way to declare some objects like Maps is different from JavaScript. This is because TypeScript supports generics. Generics in TypeScript allows us to pass in an indeterminate type to functions, interfaces, and classes (which are just syntactic sugar for functions) so that the actual type can be passed as we reference the object in the code, as we did above. The declaration above is type-safe, unlike the JavaScript way to declare Map objects. With the code above, the keys of the Map are always strings and the values are always numbers.

With dynamically typed languages, it’s hard to determine the type that a variable has without logging it, and we might assign data that we don’t want in the variable like JavaScript. TypeScript rectifies these issues by letting us set fixed types for variables so that we’re sure of the types. It has all the basic data types of JavaScript plus the types that are exclusive to TypeScript. We’ll go into more details in other TypeScript articles, where we will explore TypeScript-only data types, interfaces, combining multiple types, and more.

Categories
JavaScript

Things that You Don’t Know About the JavaScript’s Object Constructor

In JavaScript, the Object constructor lets us create object wrapper with the given values. It will create an empty object if null or undefined is passed into the Object constructor. If the value passed into the constructor is an object already then, it will return the object.

The Object constructor has 2 properties. It has a length property that is always 1, and like all other objects, the Object constructor has a prototype to get all the property additions to the type Object.

The Object constructor has many useful methods that can be used without constructor a new object. Part 1 of this list is below:

Object.assign()

The Object.assign() method makes a shallow copy of an object. The first parameter is a target object that you copy the object to, and the second parameter accepts an object that you want to copy. Note that if the source and target objects have the same properties, the source object’s property value to overwrite the one in the target object. For example, we can write:

const target = { a: 1, b: 2 };  
const source = { b: 3, c: 4};
const newObj = Object.assign(target, source);  
console.log(newObj)

If we run the code, we will get { a: 1, b: 3, c: 4} . We can also copy arrays. For example, we can write:

const targetArr = [1,2];  
const sourceArr = [2,3];
const newArr = Object.assign(targetArr, sourceArr);  
console.log(newArr)

We get [2,3] logged when we run the code above. For arrays, it will overwrite the whole target array with the source array.

Object.create()

The Object.create() method creates a new object with the object you pass in as the prototype of the new object. For example, we can write:

const obj = { a: 1, b: 2, c: 3 };  
const newObj = Object.create(obj);  
console.log(newObj);

In the code above, when we log newObj, it doesn’t have its own properties that weren’t inherited from obj. This is because we only passed in the first argument to the constructor, which is the prototype for the object that’s returned. If we want to add properties that are available only in the returned object, we pass in an object with the property names as keys and the properties writable, configurable, enumerable and value as properties of the property name keys, which is called the property descriptor. For example, we can write:

const obj = {  
  a: 1,  
  b: 2,  
  c: 3  
};

const childObj = {  
  a: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  },  
  d: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  }  
}  
const newObj = Object.create(obj, childObj);  
console.log(newObj);

In the code above, writable means that the property’s value can be changed. configurable means the property descriptor may be changed and if the property can be deleted from the object. The enumerable property means that the property shows up during enumeration of the properties with the for...in loop, and value is the value of the property.

If we want to create an object that has properties that can’t be changed, then we set writable to false, like in the following code:

const obj = {  
  a: 1,  
  b: 2,  
  c: 3  
};

const childObj = {  
  a: {  
    writable: false,  
    configurable: true,  
    value: 'hello'  
  },  
  d: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  }  
}  
const newObj = Object.create(obj, childObj);  
newObj.a = 1;  
newObj.d = 1;  
console.log(newObj);

Notice that the assignment operator has no effect. If we have strict mode enabled, then a TyepError will be thrown. We have {a: “hello”, d: 1} logged in the console.log . This means that writable set to false is working for the property a and writable value set to true is working for the property d .

If we pass in null to the constructor, we get an empty object:

const nullObj = Object.create(null)  
console.log(nullObj)

We will get an error ‘Object prototype may only be an Object or null: undefined’ is we pass in undefined as the prototype of an object like in the code below:

const undefinedObj = Object.create(undefined);  
console.log(undefinedObj)

Object.defineProperty()

The Object.defineProperty() method defines a new property on an object. The first parameter is the object that you want to add the property to. The second parameter is the name of the property you want to add passed in as a string, and the last parameter is the property descriptor included in the Object.create() method when we try to add properties to the returned object. The property descriptor should have the properties writable , configurable , enumerable and value as properties of the property name keys. The writable means that the property’s value can be changed. configurable means the property descriptor may be changed and if the property can be deleted from the object. The enumerable property means that the property shows up during enumeration of the properties with the for...in loop, and value is the value of the property. For example, we can write:

let obj = {};
Object.defineProperty(obj, 'a', {  
  writable: false,  
  configurable: true,  
  value: 'hello'  
})console.log(obj.a)  
obj.a  = 1;  
console.log(obj.a)

As we can see, the property descriptor acts the same way as in Object.create(). When writable is false, assignment to the property has no effect. If we have strict mode enabled, then a TyepError will be thrown.

We can also define getters and setter function for properties called get and set respectively for a property:

let obj = {};  
let value;Object.defineProperty(obj, 'a', {  
  get() {  
    return value;  
  },  
  set(a) {  
    value = a;  
  }  
});console.log(obj.a)  
obj.a = 1;  
console.log(obj.a)

As we can see, in the code above, we defined the property a for the object obj with the get and set functions to get the value of the property and set the value respectively.

Accessor properties are set on the prototype if we defined it on the prototype, but value properties are set on the current object. If an object inherits non-writable properties, it will still be non-writable on the current object. For example, if we have:

let ObjClass = function() {};  
ObjClass.prototype.a = 1;
Object.defineProperty(ObjClass.prototype, "b", {  
  writable: false,  
  value: 1  
});
const obj = new ObjClass();  
ObjClass.prototype.a = 3  
obj.a = 2  
ObjClass.prototype.b = 3  
obj.b = 2  
console.log(obj);

Then assigning to property b is no effect. If we have strict mode enabled, then a TyepError will be thrown.

Object.defineProperties()

The Object.defineProperties method let us define more than one property on an object. The first parameter of the method is the object that you want to define the properties on, and the second object contains the property names as key and the corresponding property descriptors as values. The property descriptor should have the properties writable, configurable, enumerable and value as properties of the property name keys. The writable means that the property’s value can be changed. configurable means the property descriptor may be changed and if the property can be deleted from the object. The enumerable property means that the property shows up during enumeration of the properties with the for...in loop, and value is the value of the property. For example, we can write:

let obj = {}  
Object.defineProperties(obj, {  
  a: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  },  
  d: {  
    writable: false,  
    configurable: true,  
    value: 'hello'  
  }  
})  
obj.a = 1;  
obj.d = 1;  
console.log(obj);

To add the a and d properties where a ‘s value can change and d ‘s value can’t. The console.log will show {a: 1, d: “hello”} since the value assignment to d fails because the writable property of property d ‘s descriptor is set to false . We can also set configurable to false to prevent it from being deleted or having its property descriptor changed. For example, we can write:

let obj = {}  
Object.defineProperties(obj, {  
  a: {  
    writable: true,  
    configurable: true,  
    value: 'hello'  
  },  
  d: {  
    writable: false,  
    configurable: false,  
    value: 'hello'  
  }  
})  
obj.a = 1;  
delete obj.d;  
console.log(obj);

If we run the code above, then we can see that the delete operator had no effect on property d of obj.

Object.entries()

The Object.entries() method returns an array of key-value pairs of an object that are enumerable by the for...in loop. They are returned in the same order as they are in if they are iterated with the for...in loop. Each entry of the array will have the key as the first element and the value of the corresponding key as the second element. For example, we can use it to loop through the properties of an object in the following code:

const obj = {  
  a: 1,  
  b: 2  
};
for (let [key, value] of Object.entries(obj)) {  
  console.log(key, value);  
}

When we run the code above, we get a 1 and b 2. These are the key-value pairs of this object.

Object.freeze()

The Object.freeze() method freezes an object. This means that all properties’ values in an object can’t be changed. Also, new properties can’t be added to it, and existing property descriptors for the frozen object can’t be changed. The object is frozen in place. This method doesn’t return a new frozen object. Instead, it returns the original object before it’s frozen. The frozen object’s prototype also can’t be changed. For example, if we run the following to freeze an object:

const obj = {  
  a: 1  
};
Object.freeze(obj);
obj.a = 2;
console.log(obj.a);

We get that obj.a is still 1. This is because the object’s properties’ values can’t be changed. If we have strict mode enabled, a TypeError will be raised. It’s important to note that values that are objects can still be modified. For example, if we have the following code:

const obj = {  
  a: 1,  
  b: {  
    c: 2  
  }  
};
Object.freeze(obj);
obj.b.c = 3;
console.log(obj.b.c);

Then obj.b.c is 3 since Object.freeze doesn’t freeze properties that are inside of nested objects unless they’re frozen explicitly. So if we have:

const obj = {  
  a: 1,  
  b: {  
    c: 2  
  }  
};
Object.freeze(obj);  
Object.freeze(obj.b);
obj.b.c = 3;
console.log(obj.b.c);

Then obj.b.c is 2 since we frozen obj.b so that we can’t modify the value of obj.b .

The Object constructor has many more methods for constructing objects from an array of arrays with key and value of properties, and also methods to get property descriptors from objects, property names, property symbols, gets the keys of objects and prevent properties from being added or deleted or modify their property descriptors.