Categories
JavaScript TypeScript

TypeScript Advanced Types — Conditional Types

TypeScript has many advanced type capabilities and which makes writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of TypeScript. There are multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more.

In this article, we’ll look at conditional types.

Conditional Types

Since TypeScript 2.8, we can define types with conditional tests. This lets us add types to data that can have different types according to the condition we set. The general expression for defining a conditional type in TypeScript is the following:

T extends U ? X : Y

T extends U describes the relationship between the generic types T and U . If T extends U is true then the X type is expected. Otherwise, the Y type is expected. For example, we can use it as in the following code:

interface Animal {    
  kind: string;  
}

interface Cat extends Animal {  
  name: string;  
}

interface Dog {  
  name: string;  
}

type CatAnimal = Cat extends Animal ? Cat : Dog;  
let catAnimal: CatAnimal = <Cat>{  
  name: 'Joe',  
  kind: 'cat'  
}

In the code above, we created the CatAnimal type alias which is set to the Cat type if Cat extends Animal . Otherwise, it’s set to Dog . Since Cat does extend Animal , the CatAnimal type alias is set to the Cat type.

This means that in the example above if we change <Cat> to <Dog> like we do in the following code:

interface Animal {    
  kind: string;  
}

interface Cat extends Animal {  
  name: string;  
}

interface Dog {  
  name: string;  
}

type CatAnimal = Cat extends Animal ? Cat : Dog;  
let catAnimal: CatAnimal = <Dog>{  
  name: 'Joe',  
  kind: 'cat'  
}

We would get the following error message:

Property 'kind' is missing in type 'Dog' but required in type 'Cat'.(2741)

This ensures that we have the right type for catAnimal according to the condition expressed in the type. If we want to Dog to be the type for catAnimal , then we can write the following instead:

interface Animal {    
  kind: string;  
}

interface Cat  {  
  name: string;  
}

interface Dog extends Animal {  
  name: string;  
}

type CatAnimal = Cat extends Animal ? Cat : Dog;  
let catAnimal: CatAnimal = <Dog>{  
  name: 'Joe'  
}

We can also have nested conditions to determine the actual type from multiple conditions. For example, we can write:

interface Animal {    
  kind: string;  
}

interface Bird  {  
  name: string;  
}

interface Cat  {  
  name: string;  
}

interface Dog extends Animal {  
  name: string;  
}

type AnimalTypeName<T> =  
  T extends Animal ? Cat :      
  T extends Animal ? Dog :      
  T extends Animal ? Bird :  
  Animaltype t0 = AnimalTypeName<Cat>;    
type t1 = AnimalTypeName<Dog>;  
type t2 = AnimalTypeName<Animal>;  
type t3 = AnimalTypeName<Bird>;

Then we get the following types for the type alias t0 , t1 , t2 , and t3 :

type t0 = Animal  
type t1 = Cat  
type t2 = Cat  
type t3: Animal

The exact doesn’t have to be chosen immediately, we can also have something like:

interface Foo {}

interface Bar extends Foo {  
    
}

function bar(x) {  
  return x;  
}

function foo<T>(x: T) {  
  let y: T extends Foo ? string : number = bar(x);  
  let z: string | number = y;  
}

foo<Bar>(1);  
foo<Bar>('1');  
foo<Bar>(false);

As we can see we can pass in anything into the foo even though we have the conditional types set. This is because the actual type in the type condition hasn’t been chosen yet., so TypeScript doesn’t make any assumption about what we can assign to the variables in the foo function.

Distributive Conditional Types

Conditional types are distributive. If we have multiple conditional types that can possibly extend one type as we have in the following code:

interface A {}  
interface B {}  
interface C {}  
interface D {}  
interface X {}  
interface Y {}type TypeName = (A | B | C) extends D ? X : Y;

Then the last line is equivalent to:

(A extends D ? X : Y) | (B extends D ? X : Y) | (C extends D ? X : Y)

For example, we can use it to filter out types with various conditions. For example, we can write:

type Diff<T, U> = T extends U ? never : T;

To remove types from T that are assignable to U . If T extends U, then the Diff<T, U> type is never, which means that we can assign anything to it, otherwise it takes on the type T. Likewise, we can write:

type Filter<T, U> = T extends U ? T : never;

to remove types from T that aren’t assignable to U . In this case, if T extends U, then the Filter type is the same as the T type, otherwise, it takes on the never type. For example, if we have:

type Diff<T, U> = T extends U ? never : T;  
type TypeName = Diff<string| number | boolean, boolean>;

Then TypeName has the type string | number . This is because Diff<string| number | boolean, boolean> is the same as:

(string extends boolean ? never : string) | (number extends boolean ? never: number) | (boolean extends boolean ? never: boolean)

On the other hand, if we write:

type Filter<T, U> = T extends U ? T : never;  
type TypeName = Filter<string| number | boolean, boolean>;

Then TypeName has the boolean type. This is because Diff<string| number | boolean, boolean> is the same as:

(string extends boolean ? string: never) | (number extends boolean ? number: never) | (boolean extends boolean ? boolean: never)

Predefined Conditional Types

TypeScript 2.8 has the following predefined conditional types, They’re the following:

  • Exclude<T, U> – excludes from T those types that are assignable to U.
  • Extract<T, U> – extract from T those types that are assignable to U.
  • NonNullable<T> – exclude null and undefined from T.
  • ReturnType<T> – get the return type of a function type.
  • InstanceType<T> – get the instance type of a constructor function type.

Since TypeScript 2.8, we can define types with conditional tests. The general expression for defining a conditional type in TypeScript is T extends U ? X : Y . They’re distributive, so (A | B | C) extends D ? X : Y; is the same as (A extends D ? X : Y) | (B extends D ? X : Y) | (C extends D ? X : Y) .

Categories
JavaScript

More Lodash Features that are Available in Plain JavaScript

In recent years, new features in JavaScript have been rolling out at a rapid pace. The deficiencies that are filled in by other libraries before have become built-in features of plain JavaScript.

In this article, we’ll look at the methods in Lodash that are now available in plain JavaScript, like function currying, partially applied functions and more.

Some features are better with Lodash but for others, plain JavaScript will suffice.

Curry

The curry method in Lodash returns a function that has one or more arguments of the function originally passed in. We can use it as follows:

const subtract = (a, b) => a - b;  
const currySubtract = _.curry(subtract);  
const subtract1 = currySubtract(1);  
const diff = subtract1(5);  
console.log(diff);

In the code above, we defined the subtract function which returns the first parameter subtracted by the second.

Then we called the curry method with the subtract method passed in to create a new method that makes one argument and returns the subtract function with the first argument set by the parameter. That’s the currySubtract function.

Then we call the currySubtract to set the argument of the subtract function and return the function with the first argument set. Finally, we call the subtract1 function with the second argument of subtract to get the final result.

We can do the same thing with plain JavaScript by writing:

const currySubtract = a => b => a - b;  
const subtract1 = currySubtract(1);  
const diff = subtract1(5);  
console.log(diff);

It does exactly the same thing, but without calling the curry method.

Partial

Lodash also has a method for partially applying a function, which is different from curry since some of the arguments of the function are passed into the function directly and the new function is returned.

For example, we can write the following:

const add = (a, b) => a + b;  
const add1 = _.partial(add, 1);  
const sum = add1(2);  
console.log(sum);

The partial method passed in the first argument and returns the function with the first argument passed in. This gets us the add1 function.

Then when can call the add1 function with the second argument, which is 2 in the code above, and we get 3 for the sum .

In plain JavaScript, we can write:

const add = (a, b) => a + b;  
const add1 = b => add(1, b);  
const sum = add1(2);  
console.log(sum);

Again, we can skip the Lodash partial method call like we did with the curry method call.

Eq

Lodash has the eq method to compare values. For example, we can write:

const equal = _.eq(1, 1);

It does the same thing as the Object.is, so we can just use that.

Add

It also has the add method, which we can use as we do in the following code:

const sum = _.add(1, 1);

We see the value is 2. It does the same thing as the + operator, so we can use that instead.

Nesting Operators

The good thing is that we can pass these methods straight into other Lodash methods like map and reduce as follows:

const mult = _.map([1, 2, 3], n => _.multiply(n, 2));

We get [2, 4, 6] from the code above, and we get 6 from:

const sum = _.reduce([1, 2, 3], _.add);

At

The at method lets us access the value of the properties of an object or an entry of an array by its index.

For example, given the following object, we can write the following:

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

We can get the value of the c property with at by writing:

const c = _.at(obj, ["a[0].b.c"]);

Then we get 2 for c .

Also, we can access more than one property of an object by passing more paths into the array above:

const vals = _.at(obj, ["a[0].b.c", "a[0].b"]);

Then we et:

2  
{c: 2}

In JavaScript, we can access the paths directly:

const vals = [obj.a[0].b.c, obj.a[0].b];

However, it’s good for access paths that may not exist. For example, given the same object, if we write the following:

const vals = _.at(obj, ["a[0].b.c", "d.e"]);

Then we get undefined for the second entry instead of crashing the app.

As we can see, Lodash still has some advantages, with object path access. However, other operators like add, multiply, curry and partial, we can define easily with plain JavaScript ourselves, so Lodash still has some value.

Categories
JavaScript Nodejs

Node.js FS Module — Read Streams

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. 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 create read streams to read a file’s data sequentially and listen to events from a read stream. Since Node.js ReadStreams are descendants of the Readable object, we will also listen to events to it.

Streams are collections of data that may not be available all at once and don’t have to fit in memory. This makes stream handy for processing large amounts of data.

It’s handy for files because files can be big and streams can let us get a small amount of data at one time. In the fs module, there are 2 kinds of streams. There’s the ReadStream and the WriteStream.

ReadStream

ReadStreams are for reading in data from a file and then outputting them a small part at a time. A ReadStream can read a small part of a file or it can read in the whole file.

To create a ReadStream, we can use the fs.createReadStream function. The function takes in 2 arguments. The first argument is the path of the file.

The path can be in the form of a string, a Buffer object, or an URL object.

The second argument is an object that can have a variety of options as properties. The flag option is the file system flag for setting the mode for opening the file. The default flag is r. The list of flags are below:

  • '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 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 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 exception is thrown if the path exists.

The encoding option is a string that sets the character encoding in the form of the string. The default value is null .

The fd option is the integer file descriptor which can be obtained with the open function and its variants. If the fd option is set, then the path argument will be ignored. The default value is null .

The mode option is the file permission and sticky bits of the file, which is an octal number that are the same as Unix or Linux file permissions. It’s only set if the file is created. The default value is 0o666. The autoClose option specifies that the file descriptor will be closed automatically. The default value is true .

If it’s false , then the file descriptor won’t be closed even if there’s an error. It’s completely up to us to close it it autoClose is set to false to make sure there’s no file descriptor leak. Otherwise, the file descriptor will be closed automatically if there’s an error or end event emitted.

The emitClose option will emit the close event when the read stream ends. The default value is false .

The start and end options specifies the beginning and end parts of the file to read. Everything in between will be read in addition to the start and end . start and end are numbers that are the starting and ending bytes of the file to read.

The highWaterMark option is limit to the number of bytes that are read in the stream. The read stream will continue to be read and buffered if the highWaterMark value is reached, but the memory usage will be high and the garbage collection performance will be poor, or it can crash your program with the Allocation failed - JavaScript heap out of memory error.

The createReadStream function returns a ReadStream object where you can attach event handlers to it.

To create a ReadStream, we can use the createReadStream like in the following code:

const fs = require("fs");  
const file = "./files/file.txt";  
const stream = fs.createReadStream(file, {  
  flags: "r",  
  encoding: "utf8",  
  mode: 0o666,  
  autoClose: true,  
  emitClose: true,  
  start: 0  
});

stream.on("open", () => {  
  console.log("Stream opened");  
});

stream.on("ready", () => {  
  console.log("Stream ready");  
});

stream.on("data", data => {  
  console.log(data);  
});

stream.on("readable", () => {  
  while ((chunk = stream.read())) {  
    console.log(chunk);  
  }  
});

stream.on("close", () => {  
  console.log("Stream closed");  
});

When we run the code above, we should get something like the following outputted to the screen, assuming that you have ‘datadatadatadata’ written your a files.txt file:

Stream opened  
Stream ready  
datadatadatadata  
datadatadatadata  
Stream closed  
Stream closed

ReadStream Events

With a ReadStream, we can listen to the following events. There’s a close event that is emitted when the close event is emitted after the file is read.

The open event is emitted when the stream is opened. The file descriptor number fd will be passed with the event when it’s emitted. The ready event is emitted when the ReadStream is ready to be used. It’s fired immediately after the open event is fired.

The ReadStream extends the stream Readable object, which emits events of its own. The data event is emitted whenever the stream data is sent to the consumer. It’s emitted when the readable.pipe() function or readable.resume() are called, or by attaching a listener callback to the data event.

The data event will also be emitted when the readable.read() function is called and a chunk of data is available to be returned. The end event is emitted when there’s no more data to be consumed from the stream. It won’t be emitted until the data is completely consumed.

This can be done by switch the stream to flowing more or calling stream.read() repeated until all the data are consumed.

The error event is emitted whenever an error occurs during the streaming or consumption of the stream. It can be because the stream can’t generate data due to internal failure or a stream attempts to push invalid chunks of data. The pause event is emitted whenever ReadStream.pause() is called and readableFlowing isn’t false .

readableFlowing can have one of 3 states. One is null. When it’s null , this means that no mechanism for consuming the stream’s data is provided and therefore the stream won’t generate data.

When readableFlowing is null, attaching a listener for the 'data' event, calling the readable.pipe() method, or calling the readable.resume() method will switch readable.readableFlowing to true, causing the ReadStream to start emitting events as data is generated.

Calling readable.pause(), readable.unpipe(), or receiving backpressure, which is the situation where data fills the buffer, readable.readableFlowing to be set as false, temporarily halting the flow of events but not halting the generation of data.

Attaching a listener for the 'data' event will not switch readable.readableFlowing to true when readable.readableFlowing is set as false .

The readable event is emitted when there’s data available to be read from the stream or the end of the stream has been reached. Attaching an event listener for the readable event may cause some amount of data to be read into an internal buffer.

It will also be emitted when the end of the stream is reached but before the end event is emitted. The resume event is emitted when ReadStream.resume() is called and the readableFlowing isn’t true .

A ReadStream object also has the following properties. The bytesRead property let us get the number of bytes read so far.

The path property is a string or a buffer that gets us the reference to the file. It’s the same as the first argument of createReadStream() .

The data type will also be the same as what we pass in as the first argument. The pending property is a boolean which is true if the underlying file hasn’t been opened yet, or before the ready event is emitted.

By using the fs.createReadStream function, we created read streams to read a file’s data sequentially and listen to events from a read stream. Since Node.js ReadStreams are descendants of the Readable object, we will also listen to events to it.

We have lots of control over how the read stream is created. We can set the path or file descriptor of the file. Also, we can set the mode of the file to be read and the permission and sticky bit of the file being read.

Also, we can choose to close the streams automatically or not or emit close event automatically. We can also set the highWaterMark option which sets the event of maximum buffer size for storing the read data.

Also, we can call pipe to move data to a writable stream, and pause the streaming of data with the pause function, and resume streaming with the resume function.

Categories
Flow JavaScript

JavaScript Type Checking with Flow — Arrays and Tuples

Flow is a type checker made by Facebook for checking JavaScript data types. It has many built-in data types we can use to annotate the types of variables and function parameters.

In this article, we’ll look at how to add Flow types to arrays and tuples.

Array Definition

We define arrays exactly like in JavaScript. To define an array, we can use the literal:

let arr = [1, 2, 3];

We can also use the Array constructor as follows:

new Array(3);

Array constructor with one argument will create an empty array with the given length, so new Array(3) returns:

[empty × 3]

Calling the Array constructor with more than one argument creates an array with the arguments as the content, so:

new Array('foo', 'bar')

will get:

["foo", "bar"]

We can also add array entries after it’s defined:

let arr = [];  
arr[0] = 'foo';

Array Type

We can type arrays by using the Array<Type> syntax, where Type is any type that we want the array entries to be.

For example, we can define a number array by writing:

let arr: Array<number> = [100, 200];

We can type an array with different types of values by using the mixed type as the type. For instance, we can write:

let arr: Array<mixed> = [1, true, "foo"];

Array Type Shorthand

We can use the Type[] syntax as a shorthand for typing array. For example, we can write:

let arr: number[] = [100, 200];

to declare a number array.

We can declare an array that can be null by putting a question mark before the type name. For example, we can write:

let arr: ?number[] = null;

Also, we can assign a number array to it:

let arr: ?number[] = [1, 2];

If we want an array to take on null values in addition to values with the given type, we can put parentheses around the type name. For example, if we want a numerical array that can have null entries, we can write:

let arr: (?number)[] = [1, 2, null];

Accessing Array Values

Flow doesn’t check whether an array entry is defined before we access it. Therefore, we can run into undefined values at runtime, which may cause errors.

For instance, we can write:

let arr: number[] = [1, 2];  
let num = arr[3];

We should check if entries are undefined before accessing them.

Read Only Arrays

We can define read-only arrays by writing $ReadOnly<T> , where T is the placeholder for the type that we want the array values to take on.

For example, we can define a numerical read-only array by writing:

const arr: $ReadOnlyArray<number> = [100, 200];

We can only read the values from the arr array.

Also, read-only arrays can take on object types. For example, we can write:

const arr: $ReadOnlyArray<{foo: number}> = [{foo: 1}, {foo: 2}];

In the case of object entries, we can modify the property value of the object’s existing properties as follows:

arr[0].foo = 2

Note that read-only arrays have subtypes of a union type. For example, we $ReadOnlyArray<number> is a subtype of $ReadOnlyArray<number | string> .

However, Array<number> isn’t a subtype of Array<number | string> .

For example, we can write:

const foo = (arr: $ReadOnlyArray<number | string>) => {  
    
}const array: Array<number> = [1]  
foo(array)

In the code above, array will be converted to a $ReadOnlyArray<number | string> as it’s passed in to foo . This is because read-only arrays can’t have its entries modified to types that aren’t specified, so Flow allows it to be passed in.

Tuples

Tuples are lists with a small number of items. In JavaScript, we create a tuple with arrays.

With Flow, we have a tuple type to type these kinds of arrays. For example, we can write:

let tuple: [string, boolean] = ['foo', true];

Then the first entry always is a string and the second is always a boolean.

We can access entries by its index, so given the tuple that we have above, we can write:

let str : string  = tuple[0];

We can’t access indexes that aren’t defined in the tuple. For example, the following code will fail:

let str : string  = tuple[3];

since we don’t have 4 or more elements in our tuple.

Flow doesn’t know the type of value we try to access, so whatever the value is will have the union of all the types in the tuple.

For example, the value of tuple we have above will be of type string | boolean :

let val: string | boolean = tuple[0];

When we set new values to a tuple entry, it must match the type of that’s defined in that location. For example, if we have:

let tuple: [number, boolean, string] = [1, false, "foo"];

Then whatever we assign to tuple[0] must be a number, tuple[1] must be a boolean, and tuple[2] must be a string.

Strictly Enforced Length

Lengths of tuples are strictly enforced, so we can assign a tuple of one length to one with different length.

For example, the following assignment will fails since tuple1 and tuple2 have different lengths:

let tuple1: [number, boolean, string] = [1, true, 'foo'];  
let tuple2: [number, boolean] = tuple1;

Tuples aren’t Arrays

In Flow, tuples aren’t arrays even though they look the same. We can’t use arrays methods with tuples and we can’t assign arrays to tuples regardless of length.

For example, the following will fail:

let array: number[] = [10, 20];  
let tuple: [number, number] = array;

Assigning tuples to arrays also doesn’t work. For example:

let tuple: [number, number] = [1, 2];  
let arr: number[] = tuple;

will fail.

Array methods like join or push can’t be used on tuples, so:

tuple.push(3);  
tuple.join(',');

will give errors.

Arrays and tuples are lists of items. They aren’t the same. Arrays can use array methods. Also, we specify the type for all entries at once. With tuples, we define the type for each entry. Tuples also can’t use array methods.

Arrays can have null entries if we add a question mark before the type name and parentheses around it. An array can be null if we skip the parentheses and keep the rest.

Categories
JavaScript

Using JavaScript’s Object Constructor — Part 1

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.