Classification of types in TypeScript

Types are classified into two main classes in TypeScript:

Basic or Primitive Types

  • boolean

  • number

  • string

  • array: number[], Array<number>

  • tuple: [string, number]

  • enum: enum Color {Red, Green = 2, Blue}, let c : Color = Color.blue

  • null

  • undefined

  • any

  • void: exists purely to indicate the absence of a value, such as in a function with no return value.

Complex or Non-Primitive Types

  • classes

  • interface

Type Assertions

Sometimes you’ll end up in a situation where you’ll know more about a value than TypeScript does. Usually this will happen when you know the type of some entity could be more specific than its current type.

1
2
3
4
let someValue : any = 'this is a string'

let strLength : number = (<string>someValue).length
let strLength : numner = (someValue as string).length

Complex Types in TypeScript

  • Interface

  • Class Types

  • Function Types

  • Indexable Types

  • Classes

Excess Property

If SquareConfig can have color and width propertes, but could also have any number of other properties, then we could define it like so:

1
2
3
4
5
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}

Class Types

Implementing an interface

One of the most common use of interfaces in language like C# and java, that of explicitly enforcing that a class meets a particular contract, is also possible in TypeScript.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface ClockInterface {
currentTime: Date,
setTime(d: Date);
}

class Clock implements ClockInterface {
currentTime: Date;
setDate(d: Date) {
this.currentTime = d;
}
constructor (h: number, m: number) {

}
}

Difference between the static and instance sides of classes

When working with classes and interfaces, it helps to keep in mind that a class has two types: the type of the static side and the type of the instance side.

You may notice that if you create an interface with a constructor signature and try to create a class that implements this interface you get an error:

This is because when a class implements an interface, only the instance side of the class is checked. Since the constructor sits in the static side, it is not included in this check.

Instead, you would need to work with the static side of the class directly. In this example, we define two interfaces, ClockConstructor for the constructor and Clockinterface for the instance methods.

1
2
3
4
5
6
7
8
9
10
11
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}

interface ClockInterface {
tick()
}

function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute)
}

Function Types

To describe a function type with an interface, we give the interface a call signature. This is like a function declaration with only the parameter list and return type given. Each parameter in the parameters list requires both name and type.

1
2
3
interface SearchFunc {
(source: string, subString: string): boolean;
}

Once defined, we can use this function type interface like we would other interfaces.

1
2
3
4
5
let mySearch: SearchFunc
mySearch = function (source, subString) {
let result = source.search(subString)
return result > -1
}

For function types to correctly type-check, the names of the parameters do not need to match. We could have, for example, written the above example like this:

1
2
3
4
5
let mySearch: SearchFunc
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub)
return result > -1
}

Function parameters are checked one at a time, with the type in each corresponding parameter position checked against each other. If you do not want to specify types at all, TypeScript’s contextual typing can infer the argument types since the function value is assigned directly to a variable of type SearchFunc

1
2
3
4
5
let mySearch: SearchFunc
mySearch = function (src, sub) {
let result = src.search(sub)
return result > -1
}

Indexable Types

Intexable types have an index signature that describes the types we can use to index into the object, along with the corresponding return types when indexing.

1
2
3
4
5
6
interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = ['Bob', 'Fred'];

Above we have a StringArray interface that has an index signature. This index signature states that when a StringArray is indexed with a number, it will return a string.

There are two types of supported index signature: string an number. It is possible to support both types of indexer, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing witha number, javascript will actually convert that to a string before indexing into a object.

Classes

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
greeting: string;
constructor (message: string) {
this.greeting = message
}
greet () {
return "Hello, " + this.greeting;
}
}

let greeter = new Greeter('World')

This class has three members: a property called greeting, a constructor, and a method greet.

In the last line we construct an instance of the Greeter class using new. This calls into the constructor we defined earlier, creating a new object with the Greeter shape, and running the constructor to initialize it.

Public, Private, and Protected Modifiers

Public by default.

When a member is marked private, it cannot be accessed from outside of its containing class.

When comparing types that have private and protected members, TypeScript treats these types differently. For two types to be considered compatible, if one of them has a private member, then the other must have a private member that originated in the same declaration.

The protected modifier acts much like the private modifier with the exectpion the members declared protected can also be accessed by instance of deriving classes.

A constructor may also be marked protected. This means that the class cannot be instantiated outside of its containing class, but can be extended.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person {
protected name: string;
protected constructor (theName: string) {
this.name = theName
}
}

// Employee can extend Person
class Employee extends Person {
private department: string;
constructor (name: string, department: string) {
super(name)
this.department = department
}
}

let howard = new Employee('Howard', 'Sales')
let john = new Persno('john') // Error, the 'Person' constructor is protceted.

You can make properties readonly by using the readonly keyword. Readonly properties must be initilized at their declaration or in the constructor.

Parameter Propertes

Parameter Properties let you create and initialize a member in one place.

Accessors

TypeScript supports getter/setters as a way of intercepting accesses to a member of an object. This gives you a way of having finer-grained control over how a member is accessed on each object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let passcode = 'secret passcode'

class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName
}
set fullName (newName: string) {
if (passcode && passcode === 'secret passcode') {
this._fullName = newName
} else {
console.log('Error: Unauthorized update of employee')
}
}
}

Accessors with a get and no set are automatically inferred to be readonly.

Static Properties

Static Members of a class are those visible on class itself rather than on the instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Grid {
static origin = {x: 0, y: 0}
calculatedDistanceFromOrigin(point: { x:number, y: number }) {
let xDist = (point.x - Grid.origin.x)
let yDist = (point.y - Grid.origin.y)
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale
}

constructor (public scale: number) {
// ...
}
}

let grid1 = new Grid(1.0)
let grid2 = new Grid(5.0)

Abstract Classes

Abstract Classes are base classes from which other classes may be derived. They may not be instantiated directly. Unlike an interface, an abstract class may contain implementation details for its members. The abstract keyword is used to define abstract classes as well as abstract methods within an abstract class.

1
2
3
4
5
6
abstract class Animal {
abstract makeSound(): void;
move () : void {
console.log {'roaming the earth...'}
}
}

Methods within an abstract class that are marked as abstract do not contain an implementation and must be implemented in derived classes. Abstract methods share a similar syntax to interface methods. Both define the signature of a method without including a method body. However, abstract methods must include the abstract keyword and may optionally include access modifier.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
abstract class Department {
constructor (public name: string) {
// ...
}

printName () : void {
console.log('Department name: ' + this.name)
}
abstract printMeeting (): void // must be implemented in derived classes
}

class AccountingDepartment extends Department {
constructor () {
super('Accounting and Auditing')
}
printMeeting () : void {
console.log('The Accounting Department meets each Monday at 10am')
}
generateReports () : void {
console.log('Generating accounting reports...')
}
}

let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName()
department.printMeeting()
department.generateReports()

Advanced Techniques

Constructor functions

When you declare a class in TypeScript, you are actually creating multiple delcaration at the same time. The first is the type of the instance of the class

1
2
3
4
5
6
7
8
9
10
11
12
13
class Greeter {
greeting: string;
constructor (message: string) {
this.greeting = message
}
greet () {
return 'Hello, ' + this.message
}
}

let greeter: Greeter
greeter = new Greeter('world')
console.log(greeter.greet())

Here, when we say let greeter: Greeter we’re using Greeter as the type of instance of the class Greeter. This is almost second nature to programmers from other oo language.

We’re also creating another value that we call the constructor function. This is the function that is called when we new up instance of the class. To see what this looks in practice, let’s take a look at the JavaScript created the above example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let Greeter = (function () {
function Greeter(message) {
this.greeting = message
}

Greeter.prototype.greet = function () {
return 'Hello, ' + this.greeting
}
return Greeter
})()

let greeter
greeter = new Greeter('world')
console.log(greeter.greet())

Here let Greeter is going to be assigned the constructor function. When we call new and run this function, we get an instance of the class. The constructor function also contains all of the static members of the class. Another way to think of each class is that there is an instance side and a static side.

Using a class as an interface

As mentioned above, a class declaration creates two things: a type representing instances of the class, and a constructor function. Because classes creates types, you can use them in the same places you would be able to use interfaces:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Point {
x: number;
y: number;
}

interface Point3D extends Point {
z: number;
}

let point3d: Point3D = {
x: 1,
y: 2,
z: 3
}

Inheritance

In TypeScript, we can use common object-orientad pattern. Of course, one of the most fundamental patterns in class-based programming is being able to extend existing classes to create new ones using inheritance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Animal {
name: string;
constructor (theName: string) {
this.name = theName
}
move (distanceInMeters: number = 0) {
console.log(`${this.name} move ${distanceInMeters}`)
}
}

class Snake extends Animal {
constructor (name: string) {
super(name)
}

move (distanceInMeters = 5) {
console.log('Slithering...')
super.move(distanceInMeters)
}
}

class Horse extends Animal {
constructor (name: string) {
super(name)
}
move (distanceInMeters = 45) {
console.log('Galloping...')
super.move(distanceInMeters)
}
}

Derived classes that contain constructor functions must call super() which will execute the constructor function on the base class.

Interfaces Extending Classes

When an interface type extends a class type it inherits the memembers of the class but not their implementations. It is as if the interface had declared all of the members of the class without providing an implementation. Interface inherit event the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected memebrs, that interface type can only be implemented by the class or a subclass of it.

This is useful when you have a large inheritance hierarchy, but want to specify that you code works iwth only subclasses that have certain properties. The subclasses don’t have to be related besides inheriting from the base class.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Control {
private state: any;
}

interface SelectableControl extends Control {
select () : void;
}

class Button extends Control {
select () {}
}

class TextBox extends Control {
select () {}
}

class Image {
select () {}
}

class Location {
select () {}
}

In the above example, SelectableControl contains all of the members of Control, including the private state property. Since state is a private member it is only possible for descendants of Control to implement SelectableControl. This is because only descendants of Control will have a state private member that originates in the same declaration, which is a requirement for private members to be compatible.

Within the Control class it is possible to access the state private member through an instance of SelectableControl. Effectively, a SelectableControl acts like a Control that is known to have a select method. The Button and TextBox classes are subtypes of SelectableControl, but Image and Location are not.

Type Compatibility

Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based on solely on their members. This is in contrast with normal typing.

1
2
3
4
5
6
7
8
9
10
11
12
interface Named {
name: string;
}

class Person {
name: string;
}

let p: Named;

// OK, because of structural typing
p = new Person()

A basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x.

1
2
3
4
5
6
7
8
9
interface Named {
name: string;
}

let x: Named;

// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' }
x = y

Comparing Two Functions

While comparing primitive types and object types is relatively straightforward, the question of what kinds of functions should be considered compatible is a bit more involved.

1
2
3
4
5
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;

y = x // ok
x = y // Error

To check if x is assignable to y, we first look at the parameter list. Each parameter in x must have a corresponding parameter in y with a compatible type. Note that the name of the parameters are not considered, only their types. In this case, every parameter of x has a corresponding compatible parameter in y, so x is assignable to y.

To ensure that every parameter in x can be checked in y.

Enums

Enums are compatible with numbers, and numers are compatible with enums.

Enum values from different enum types are considered incompatible.

Classes

Classes work similarly to object literal types and interfaces with one exception: they have both a static and an instance type. When comparing two objects of a class type, only members of the instance are compared. Static memebrs and constructor do not affect compatibility.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Animal {
feet: number;
constructor (name: string, numFeet: number) {

}
}

class Size {
feet: number;
constructor (numFeet: number) {

}
}

let a: Animal
let s: Size

a = s // ok
s = a // ok

Private and protected members in a class affect their compatibility. When an instance of a class is checked for compatibility, if the target type contains a private member, then the source type must also contain a private member that originated from the same class.

Contextual Type

Type inference also work in ‘the other direction’ in some cases in TypeScript. This is known as ‘contextual typing’. Contextual typing occurs when the type of an expression is implied by its location.

1
2
3
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.buton) // <- Error
}

Functions and function Types

TypeScript functions can be created both as a named function or as an anonymous function. This allows you to choose the most appropriate approach for your application, whether you’re building a list of functions in an API or a one-off function to hand off to another function.

1
2
3
4
5
6
7
// Named Function
function add(x, y) {
return x + y
}

// Anonymouse Function
let myAdd = function (x, y) { return x + y }

Function Types

Typing the function.

1
2
3
function add(x: number, y: number): number {
return x + y
}

Writing the function type

1
let myAdd: (x: number, y: number) => number = function(x, y) { return x + y }

A function’s type has the same two parts: the type of the arguments and the return type. When writing out the whole function type, both parts are required.

Context and Scope

this and arrow function.

In JavaScript, this is a variable that’s set when a function is called. This makes it a very powerful and flexible feature, but it comes at the cost of always having to know about the context that a function is executing in.

1
2
3
4
5
6
7
8
9
10
11
let deck = {
suits: ['hearts', 'spades', 'clubs', 'diamonds'],
cards: Array(52),
createCardPicker: function () {
return function () {
let pickedCard = Math.floor(Math.random() * 52)
let pickedSuit = Math.floor(pickedCard / 13)
return { suit: this.suits[pickedSuit], card: pickedCard % 13 }
}
}
}

Here the function returned by createCardPicker will be called by window

1
2
3
4
5
6
7
8
9
10
11
12
let deck {
suits: ['hearts', 'spades', 'clubs', 'diamonds'],
cards: Array(52),
createCardPicker: function () {
// NOTE: the line below is now an arrow function, which will capture 'this' when then function is declared.
return () => {
let pickedCard = Math.floor(Math.random() * 52)
let pickedSuit = Math.floor(pickedCard / 13)
return { suit: this.suits[pickedSuit], card: pickedCard % 13 }
}
}
}

Overloads

JavaScript is inherently a very dynamic language. It’s not uncommon for a single JavaScript function to return different types of objects based on the shape of the arguments passed in.

1
2
3
4
5
6
7
8
9
10
11
12
13
let suits = ['hearts', 'spades', 'clubs', 'diamonds']

function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == 'object') {
let pickedCard = Math.floor(Math.random() * x.length)
return pickedCard
} else if (typeof x === 'number') {
let pickedSuit = Math.floor(x / 13)
return { suit: suits[pickedSuit], card: x % 13 }
}
}

Here the pickCard function will return two different things based on what the user ahs passed in.

Use overloads.

1
2
3
4
5
6
7
8
9
10
11
12
let suits = ['hearts', 'spades', 'clubs', 'diamonds']

function pickCard(x: { suit: string, card: number }[]): number
function pickCard(x: number): { suit: string, card: number }
function pickCard(x): any {
// Check to see if we're working with an object/array
if (typeof x === 'object') {
// ...
} else if (typeof x === 'number') {
// ...
}
}

Note that the function pickCard(x): any is not part of the overload list.

Namespaces

Namespaces are used as a way to organize the code so that you can keep track of your types and not worry about name collisions with other object. Instead of putting lots of different names into the global namespace, you can wrap up your object into a namespace. The choice of what objects go in what namespaces is totally up to your organization preference.

If you want the interfaces and classes in your namespaces to be visible outside the namespace, you need to preface their declaration with export keyword.

Also keep in mind that classes, interfaces and variable names within a namespace has to be unique.

Multi-file namespaces

You can split a namespace across many files, even though the fiels a separate, they can each contribute to the same namespace and can be consumed as if they were all defined in one place. Because there are dependencies between files. you will add reference tags to tell the compiler about the relationship between the files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// ZooAnimals.ts

namespace Zoo {
interface Animal {
//NOTE: we do not need the 'export' here since this interface will only be accessible by enttites from within the Zoo namespace.
skinType: string;
isMammal() : boolean;
}
}

// ZooWild.ts
/// <reference path="ZooAnimals.ts" />
namespace Zoo {
export class Reptile implements Animal {
// NOTE: we need the 'export' here to be able to access this class and instantiate objects of the Reptile type
skinType: 'scales';
isMammal () {
return false
}
}
}

// ZooBirds.ts
/// <reference path="ZooAnimals.ts" />
namespace Zoo {jkj
export class Bird implements Animal {
skinType: 'feather'
isMammal () {
return false
}
}
}

Once there are multiple fiels involved, we’ll need to make sure all of the compiled code gets loaded, we can use concatenated output using the --outFile flag to compile all of the input files into a single JavaScript output file.

1
tsc --outFile zoo.js ZooAnimal.ts ZooWild.ts ZooBirds.ts

Referencing namespacing entities in your code

Namespaces can be accessed either in the same file where the namespace is defined or another .ts file. The only control on access is whether you are using the export keyword or not for the namespace entities.

1
let parrot = new Zoo.Bird();

Aliases

You can simplify working with namespaces by using Aliases. You can use import q = x.y.z to create shorter names for commonly-used objects.

1
2
3
import rep = Zoo.Reptitle;

let lizard: rep;