TypeScript for JavaScript Programmers – II

Typescriptlang.org
https://www.typescriptlang.org/docs/handbook/generics.html

Interfaces :

One of TypeScript’s core principles is that type checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural subtyping”. In TypeScript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.

Our First Interface
The easiest way to see how interfaces work is to start with a simple example:

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

let myObj = { size: 10, label: “Size 10 Object” };
printLabel(myObj);

The type checker checks the call to printLabel. The printLabel function has a single parameter that requires that the object passed in has a property called label of type string. Notice that our object actually has more properties than this, but the compiler only checks that at least the ones required are present and match the types required. There are some cases where TypeScript isn’t as lenient, which we’ll cover in a bit.

We can write the same example again, this time using an interface to describe the requirement of having the label property that is a string:

interface LabeledValue {
label: string;
}

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

let myObj = { size: 10, label: “Size 10 Object” };
printLabel(myObj);

Optional Properties

Not all properties of an interface may be required. Some exist under certain conditions or may not be there at all. These optional properties are popular when creating patterns like “option bags” where you pass an object to a function that only has a couple of properties filled in.

Here’s an example of this pattern:

interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = { color: “white”, area: 100 };
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}

let mySquare = createSquare({ color: “black” });

On the last line of the snippet you can see that even assigning the entire ReadonlyArray back to a normal array is illegal. You can still override it with a type assertion, though:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;

a = ro as number[];

// OUTPUT : 1


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.

Excess Property Checks

In our first example using interfaces, TypeScript lets us pass { size: number; label: string; } to something that only expected a { label: string; }. We also just learned about optional properties, and how they’re useful when describing so-called “option bags”.

interface SquareConfig {
color?: string;
width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
return {
color: config.color || “red”,
//area: config.width ? config.width * config.width : 20,
width : 100
};
}

let mySquare = createSquare({ colour: “red”, width: 100 });
Argument of type ‘{ colour: string; width: number; }’ is not assignable to parameter of type ‘SquareConfig’.
Object literal may only specify known properties, but ‘colour’ does not exist in type ‘SquareConfig’. Did you mean to write ‘color’?

Getting around these checks is actually really simple. The easiest method is to just use a type assertion:

let mySquare = createSquare({ width: 100, coluor: “green” } as SquareConfig);

console.log(mySquare.width) // 100
console.log(mySquare.coluor) // ERROR

However, combining the two naively would allow an error to sneak in. For example, taking our last example using createSquare:


However, a better approach might be to add a string index signature if you’re sure that the object can have some extra properties that are used in some special way. If SquareConfig can have color and width properties with the above types, but could also have any number of other properties, then we could define it like so:

interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}

function createSquare(config: SquareConfig): SquareConfig {
return {
color: config.color || “red”,
coluor: “red”
///area: config.width ? config.width * config.width : 20,
};
}

let mySquare = createSquare({ coluor: “red”, width: 100 } as SquareConfig) ;

console.log(mySquare.coluor) // red

We’ll discuss index signatures in a bit, but here we’re saying a SquareConfig can have any number of properties, and as long as they aren’t color or width, their types don’t matter.

One final way to get around these checks, which might be a bit surprising, is to assign the object to another variable: Since squareOptions won’t undergo excess property checks, the compiler won’t give you an error.

let squareOptions = { colour: “green”, width: 100 };
let mySquare = createSquare(squareOptions);

function createSquare(config: SquareConfig): SquareConfig {
return {
color: config.color || “red”,
width: 100,
colour:config.colour
};
}
let squareOptions = { colour: “green”, width: 100 };
let mySquare = createSquare(squareOptions);

console.log(mySquare.width) // 100
console.log(mySquare.colour) // green
but it will show error on code for colour but will give output.


The above workaround will work as long as you have a common property between squareOptions and SquareConfig. In this example, it was the property width. It will however, fail if the variable does not have any common object property. For example:

let squareOptions = { colour: “red” };
let mySquare = createSquare(squareOptions);
Type ‘{ colour: string; }’ has no properties in common with type ‘SquareConfig’.
—–
Indexable Types
Similarly to how we can use interfaces to describe function types, we can also describe types that we can “index into” like a[10], or ageMap[“daniel”]. Indexable 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. Let’s take an example:

interface StringArray {
[index: number]: string;
}

let myArray: StringArray;
myArray = [“Bob”, “Fred”];

let myStr: string = myArray[0];

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

interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}

Finally, you can make index signatures readonly in order to prevent assignment to their indices:

interface ReadonlyStringArray {
readonly [index: number]: string;
}

let myArray: ReadonlyStringArray = [“Alice”, “Bob”];

—-
Class Types

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

You can also describe methods in an interface that are implemented in the class, as we do with setTime in the below example:
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) {}
}

Interfaces describe the public side of the class, rather than both the public and private side. This prohibits you from using them to check that a class also has particular types for the private side of the class instance.

Extending Interfaces
Like classes, interfaces can extend each other. This allows you to copy the members of one interface into another, which gives you more flexibility in how you separate your interfaces into reusable components.
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

let square = {} as Square;
square.color = “blue”;
square.sideLength = 10;

An interface can extend multiple interfaces, creating a combination of all of the interfaces.

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

Because of JavaScript’s dynamic and flexible nature, you may occasionally encounter an object that works as a combination of some of the types described above.

One such example is an object that acts as both a function and an object, with additional properties:

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;

——-
Function Types

Typing the function
Let’s add types to our simple examples from earlier:

function add(x: number, y: number): number {
return x + y;
}

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

Writing the function type
Now that we’ve typed the function, let’s write the full type of the function out by looking at each piece of the function type.

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

We could have instead written:

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


Inferring the types
In playing with the example, you may notice that the TypeScript compiler can figure out the type even if you only have types on one side of the equation:

// The parameters ‘x’ and ‘y’ have the type number
let myAdd = function (x: number, y: number): number {
return x + y;
};

// myAdd has the full function type
let myAdd2: (baseValue: number, increment: number) => number = function (x, y) {
return x + y;
};

Parameters , Optional Parameters

function buildName(firstName: string, lastName: string) {
return firstName + ” ” + lastName;
}
let result3 = buildName(“Bob”, “Adams”); // ah, just right


function buildName(firstName: string, lastName?: string) {
if (lastName) return firstName + ” ” + lastName;
else return firstName;
}

let result1 = buildName(“Bob”); // works correctly now

In TypeScript, we can also set a value that a parameter will be assigned if the user does not provide one, or if the user passes undefined in its place.
function buildName(firstName: string, lastName = “Smith”) {
return firstName + ” ” + lastName;
}
let result1 = buildName(“Bob”); // works correctly now, returns “Bob Smith”
let result2 = buildName(“Bob”, undefined); // still works, also returns “Bob Smith”

Rest Parameters
In TypeScript, you can gather these arguments together into a variable:

function buildName(firstName: string, …restOfName: string[]) {
return firstName + ” ” + restOfName.join(” “);
}

// employeeName will be “Joseph Samuel Lucas MacKinzie”
let employeeName = buildName(“Joseph”, “Samuel”, “Lucas”, “MacKinzie”);
——–

Generics
********
https://www.typescriptlang.org/docs/handbook/generics.html

Leave a Reply

Your email address will not be published. Required fields are marked *