Non-Structural Types

Built-In Types in TypeScript

Primitive Types in JavaScript

In pure JavaScript, there are seven types of primitive values:

  • string,

  • symbol,

  • number,

  • bigint,

  • boolean,

  • undefined, and

  • null.

The above seven JavaScript primitive types characterize a given primitive value at runtime and can be checked dynamically at runtime using the keyword typeof.

typeof 'forty two' // => 'string'
typeof 42 // => 'number'
typeof BigInt(42) // => 'bigint'
typeof true // => 'boolean'
typeof undefined // => 'undefined'
typeof null // => 'object'

The expression typeof null returns 'object'. Although null is a primitive value it denotes an explicit indication of the absence of an object value and therefore typeof null returns 'object'.

Primitive Types in TypeScript

TypeScript have static counterparts of all JavaScript primitive types.

Therefore, the primitive types in TypeScript are:

  • string,

  • symbol,

  • number,

  • bigint,

  • boolean,

  • undefined, and

  • null.

TypeScript types characterize a given value statically i.e. before runtime.

Object Types

Objects in JavaScript

In pure JavaScript, any value that is not a primitive value must be an object.

A JavaScript object is a structural value that has properties which values reference other values of primitive and object types.

Functions are also objects in JavaScript.

An object in JavaScript can be created using:

  • literal notation - explicit syntactical depiction of a given object, or

  • the Object constructor.

Creating an Object in JavaScript

The primary way of creating an object in JavaScript is using curly braces literal notation, i.e. { and }, along with specifying zero or more properties.

const foo = {}
typeof foo // => 'object'

const bar = {
  baz: 42
}
typeof bar // => 'object'

However, it is also possible to create an object using the Object constructor.

const foo = new Object()
typeof foo // => 'object'

A way of creating an integer-keyed object, i.e. an array, is using square brackets literal notation, i.e. [ and ], along with specifying zero or more array values.

const foo = []
typeof foo // => 'object'

const bar = [42]
typeof bar // => 'object'

It is also possible to create an array using the Array constructor.

const foo = new Array(1,2,3)

Although values in arrays can be accessed using integers (0, 1, 2, etc.) in reality - under the hood - arrays are string indexed ('0', '1', '2', etc.). When accessing JavaScript array values using integers they are being cast to strings.

const foo = ['a', 'b', 'c']
foo[1] // => 'b'
foo['1'] // => 'b'
foo[1] === foo['1'] // => true

There are two ways of creating a callable object using the literal notation in JavaScript:

  • the function keyword, or

  • the arrow function syntax.

function foo() {}
typeof foo // => 'function'

const baz = () => {}
typeof baz // => 'function'

It is also possible to create a function using the Function constructor.

const add = new Function(['a', 'b'], 'return a + b')

Dynamic Object Type Checking in JavaScript

It is possible to check in JavaScript dynamically at runtime whether a given value is:

  • an object - using the typeof operator,

  • a specific integer-keyed object, i.e. an array - using Array.isArray method, or

  • a specific callable object, i.e. a function - again using typeof.

const foo = {}
typeof foo === 'object' // => true

const bar = []
typeof bar === 'object' // => true
Array.isArray(bar) // => true

const baz = () => {}
typeof baz === 'function' // => true

Further, it is possible to dynamically check in JavaScript at runtime whether a given object is an instance of another object using the instanceof operator. This allows checking whether a given object is for example an array or a function.

const foo = []
foo instanceof Array // => true
foo instanceof Object // => true
foo instanceof Function // => false

const baz = () => {}
baz instanceof Array // => false
baz instanceof Object // => true
baz instanceof Function // => true

Objects in TypeScript

During static (before runtime) code checking TypeScript allows - among other things - to restrict a given value to be:

  • an object,

  • a specific integer-keyed object, i.e. an array, or

  • a specific callable object, i.e. a function.

To restrict a given value to be an object and therefore disallow any non-object values the TypeScript type 'object' is used.

let foo: object
foo = {} // OK
foo = 42 // TS: Type 'number' is not assignable to type 'object'.

To restrict a given value to be an array and therefore disallow any non-array values the TypeScript type Array is used.

let foo: Array<any>
foo = [] // OK
foo = {} // TS: Type '{}' is missing the following properties from type 'any[]': length, pop, push, concat, and 29 more.

The Array type must specify a type of its allowable values.

For example, Array<any> allows values of any type. Array<string> allows values of string type only. Array<string | number> allows values of string or number types only.

Instead of Array<any> notation the alternative notation using square brackets can be used.

let foo: any[]
let bar: string[]
let baz: (string | number)[]

To restrict a given value to be a function and therefore disallow any non-function values the TypeScript specific notation is used.

let foo: () => void
foo = () => { console.log('Bar') } // OK
foo = {} // TS: Type '{}' is not assignable to type '() => void'. Type '{}' provides no match for the signature '(): void'.

Any

The TypeScript any type denotes a value that is allowed to be of any other TypeScript type.

A given value can be declared to be of any type:

  • explicitly, or

  • implicitly.

// Explicit declaration of bar as any.
function foo(bar: any) {
  console.log(bar)
}

// Implicit declaration of qux as any.
function baz(qux) {
  console.log(qux)
}

Whereas TypeScript always allows declaring a given value to be of any type explicitly the implicit any declaration might be disallowed using the noImplicitAny flag in tsconfig.json.

TypeScript allows calling any properties of values of any type. Compare it to the unknown type.

let foo: any
foo.bar() // TypeScript allows this but JavaScript will throw an error at runtime.

Although TypeScript will allow the above operation it will still fail during JavaScript runtime and the Uncaught TypeError: Cannot read properties of undefined (reading 'bar') error shall be thrown. This shows how overusing any might hinder the TypeScript type checking effort.

Unknown

The TypeScript unknown type denotes a value which type is not known at the time of static code checking as the value might be created dynamically during JavaScript runtime.

The difference between any and unknown is that at the time of the static code checking a value denoted with the any type is explicitly or implicitly allowed to be of any type whereas the type of value denoted with the unknown type is simply unknown which does not mean that it is allowed to be of the any type.

TypeScript does not allow calling properties of values of the unknown type. Compare it to the any type.

let foo: unknown
foo.bar() // TS: Property 'bar' does not exist on type 'unknown'.

Never

The TypeScript never type denotes a type representing a situation in which a value cannot be evaluated.

The never type is used:

  • to denote the type being returned by a function that never returns any value,

  • to denote the type of an already fully narrowed variable (from type perspective) in a specific block of code.

function foo(): never {
  throw new Error()
}

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

let baz
if (baz === 'object') {
  console.log(baz)
} else if (baz !== 'object') {
  console.log(baz)
} else {
  // Unreachable block.
  console.log(baz) // The inferred type of baz in the else block is never.
}

Void

The TypeScript void type is used to denote that a function should not return any explicit value.

The function return type of void restricts the return value of a function based on the manner of the void type annotation. When the void return type is annotated directly in a function body the function should not return any value but an explicit return of undefined or null (provided strictNullChecks is false) is allowed by TypeScript.

function foo(): void {} // OK

function bar(): void {
  return
} // OK

function baz(): void {
  return undefined // OK
}

function qux(): void {
  return null // OK provided strictNullChecks is false.
}

function quux(): void {
  return 42
} // TS: Type 'number' is not assignable to type 'void'.

However, when the void return type is a part of a function type annotation TypeScript will allow any value to be returned by the function with that type.

const foo: () => void = () => 42 // OK

type Baz = () => void
const qux: Baz = function() {
  return 42
} // OK

Further reading on the subject: Why does TypeScript have both void and undefined?