0%

Learning TypeScript From Scratch(2)

Interfaces


Type compatibility with object varibale

The compiler only checks that at least the ones required are present and match the types required when using object variables. It means that you should have all properties inside declaration, and still can have properties more than declaration.

1
2
3
4
5
6
7
8
9
10
interface LabeledValue {
label: string;
}

function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}

let myObj = { size: 10, label: "Size 10 object" };
printLabel(myObj);

Excess property check with object literal

When it comes to object literals, excess properties are not allowed. It means that you can’t have properties more than declaration.

1
2
3
4
5
6
7
8
9
interface LabeledValue {
label: string;
}

function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}

printLabel({ size: 10, label: "Size 10 object" }); // Error

However, there are some workarounds.

1
2
3
4
5
6
7
8
// Add a type assertion
printLabel({ size: 10, label: "Size 10 object" } as LabeledValue);

// Change interface declaration, named index signature
interface LabeledValue {
label: string;
[propName: string]: any;
}

* Detailed referrence.

Optional properties

Not all properties of an interface may be required, they are possibly available.

1
2
3
4
5
6
7
8
interface Config {
color?: string;
width?: number;
}

function createStuff(cfg: Config) { ... }

let myStuff = createStuff({ color: "black" });

Readonly properties

This type of properties could only be modifiable when first created.

1
2
3
4
5
6
7
interface Point {
readonly x: number;
readonly y: number;
}

let p: Point = { x: 10, y: 20 };
p.x = 5; // Error

There exists readonly array, which has all mutating methods removed.

1
2
3
4
5
6
7
8
9
10
let arr: number[] = [1, 2, 3, 4];
let readonlyArr: ReadonlyArray<number> = arr;

readonlyArr[0] = 12; // Error
readonlyArr.push(5); // Error
readonlyArr.length = 100; // Error
arr = readonlyArr; // Error: cannot be assigned to mutable array

// However, we can still override it with a type assertion
arr = readonlyArr as number[];

* readonly vs const
The easiest way to remember whether to use readonly or const is to ask whether you’re using it on a variable or a property. Variables use const whereas properties use readonly.

Function types

Names of parameters do not need to match. Types are allowed to not specified, with typescript’s contextual typing which can infer the argument types.

1
2
3
4
5
6
7
8
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string): boolean { ... }
mySearch = function (src: string, sub: string): boolean { ... }
mySearch = function (src, sub): boolean { ... }

Indexable Types

Take an example as below. 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.

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

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

let myString: string = myArray[0];

There are two types of supported index signatures: string and number. But there is a rule: the type returned from a numeric indexer must be a subtype of the type returned from a string indexer. Because numeric index will actually be converted into string index.

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

interface ASubType extends AType {
breed: string;
}

interface NotOkay {
[x: number]: AType; // Error
[x: string]: ASubType;
}

Another note is that, string index signatures enforce that all properties match their return type. Because obj.property is also available as obj["property].

1
2
3
4
5
interface Dictionary {
[index: string]: number; // Means that all propreties should be number
length: number;
name: string; // Error
}

However, properties of different types are acceptable if the index signature is a union of the property types.

1
2
3
4
5
interface Dictionary {
[index: string]: number | string;
length: number;
name: string;
}

What’s more, make index signatures readonly can prevent indeces from being assigned. (Though I’m a little bit confused…)

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

let myArray: ReadonlyStringArray = ["a", "b"];
myArray[2] = "c"; // Error

Class types

Nothing special when it goes without constructor constrained.

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

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

It’s different when it comes to having a construct signature. Below would get an error.

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

// Error:
// Class 'Clock' incorrectly implements interface 'ClockConstructor'.
// Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) {}
}

Firstly, notice that class has two types: the type of the static side and the type of the instance side.

Secondly, when a class implements an interface, only the instance side of the class is checked, while the constructor sits in the static side and is not included in the check.

So, we need to work with the static side of the class directly.

I think it might take some time to understand example below. This time, createClock‘ s frist parameter should be of type ClockConstructor, and DigitalClock/AnalogClock which is passed as param would get checked.

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
33
34
// For the constructor in the static side
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}

// For the method in the instance side
interface ClockInterface {
tick(): void;
}

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

class DigitalClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}

class AnalogClock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("tick tock");
}
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

Another way is to use class expressions:

1
2
3
4
5
6
7
...
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
};

Extending interfaces

Interfaces can extend each other, even can extend multiple interfaces.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Shape {
color: string;
}

interface PenStroke {
penWidth: number;
}

interface Square extends Shape, PenStroke {
sideLength: number;
}

let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

Hybrid types

Just a combination of above. For example, below is an object that acts as both function and object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}

function getCounter(): Counter {
let counter = function (start: number) {} as Counter;
counter.interval = 123;
counter.reset = function () {};
return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

Interfaces extending classes

Several key points:

  • interfaces would have all of the class’ members but not their implementations
  • members include the private and protected ones, but the interface thus could only be implements by that class or a subclass of it
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// The class
class Control {
private state: any;
}

// The interface(has the private state)
// Only Control class itself and its subclass can extend this interface
interface SelectableControl extends Control {
select(): void;
}

// Button and TextBox are subtypes because both inherit from Control and have the select method
class Button extends Control implements SelectableControl {
select() {}
}
class TextBox extends Control {
select() {}
}

// Error: beceuse it has its own state rather than extending Control
class ImageControl implements SelectableControl {
private state: any;
select() {}
}