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 thetypeof
operator, -
a specific integer-keyed object, i.e. an
array
- usingArray.isArray
method, or -
a specific callable object, i.e. a
function
- again usingtypeof
.
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?