This article is the first in a series of TypeScript Tutorials.

interface

In TypeScript, we use interfaces to describe the concrete structure of objects or classes. The concept of interfaces is crucial in TypeScript. It’s like a contract you have with your program. By defining an interface, you promise the program that some future value (or class) will conform to the contract, and if it doesn’t, TS will report an error on compilation.

Those of you who are interested can learn about duck types.

Here’s an example:

interface Phone {
    model: string
    price: number
}

let newPhone: Phone = {
    model: 'iPhone XS',
    price: 8599,}Copy the code

In the example above, we define an interface called Phone, which specifies that any value of type Phone can have only two attributes: model of type string and Price of type number. We then declare a variable, newPhone, of type Phone, and by contract assign Model to a string and price to a value.

Interfaces generally start with uppercase letters. In some programming languages it is recommended to use I as a prefix. Tslint has a specific rule about whether to use the I prefix, depending on your team’s coding style.

More attributes and less attributes are not allowed.

let phoneA: Phone = {
    model: 'iPhone XS',}// Error: Property 'price' is missing in type '{ model: string; }' but required in type 'Phone'

let phoneB: Phone = {
    model: 'iPhone XS',
    price: 8599,
    producer: 'Apple',}// Error: Property 'producer' doesn't exist on type `Phone`.
Copy the code

Interfaces, as type annotations, are only useful at compile time and do not appear in the final output of JS code.

Optional attribute

For a possible attribute, we can add? The tag indicates that this attribute is optional.

interface Phone {
    model: string
    price: numberproducer? :string
}

let newPhone: Phone = {
    model: 'iPhone XS',
    price: 8599.// OK
}

let phoneB: Phone = {
    model: 'iPhone XS',
    price: 8599,
    producer: 'Apple'.// OK
} 
Copy the code

Any attribute

In some cases, we may only know some of the attributes in the interface and their types. Alternatively, we want to be able to dynamically add the attributes of the object after initialization. In this case, we can use the following notation.

interface Phone {
    model: string
    price: numberproducer? :string
    [propName: string] :any
}

let phoneB: Phone = {
    model: 'iPhone XS',
    price: 8599,
} 
phoneB.storage = '256GB' // OK
Copy the code

Above, we define the signature of any attribute as string and the value as any. Note: The value type of any attribute must contain the value types of all known attributes. In the example above, any includes the string and number types.

Read-only property

In an interface, we can use readonly to indicate that a property is read-only. When we try to modify it, TS will prompt us with an error.

interface Phone {
    readonly model: string
    price: number
}

let phoneA: Phone = {
    model: 'iPhone XS',
    price: 8599,
}
phoneA.model = 'iPhone Air' // Error: Cannot assign to 'model' because it is a read-only property.
Copy the code

function

Whew, finally function. There are two ways to define functions in JavaScript.

// Name the function
function add(x, y) {
    return x + y
}

// Anonymous function
const add = function(x, y) { return x + y }
Copy the code

The way you add type annotations is pretty much the same for both methods.

// Name the function
function add(x: number, y: number) :number {
    return x + y
}

// Anonymous function
const add = function(x: number, y: number) :number { 
    return x + y
}
Copy the code

Above we defined the add function, which takes two parameters of type number and returns a value of type number.

When a function is called, the type and number of arguments passed in must be the same as when they were defined.

add(1.2) // OK
add('1'.0) // Error
add(1.2.3) // Error
Copy the code

Optional parameters

Use? The flag indicates that a parameter is optional. Optional parameters must be placed after required parameters.

function increment(x: number, step? :number) :number {
    return x + (step || 1)
}

increment(10) / / = > 11
Copy the code

Parameter Default Value

ES6 allows us to add default values for parameters. As a superset of JS, TS also supports parameter defaults.

function increment(x: number, step: number = 1) :number {
    return x + step
}

increment(10) / / = > 11
Copy the code

Because parameters that have parameter defaults are necessarily optional, they are no longer needed? Optional when marking this parameter.

Here, step: number = 1 can be abbreviated as step = 1. TS automatically concludes that step should be of the number type based on type inference.

Unlike optional parameters, parameters with default values do not have to be placed after required parameters. The following is also allowed, except that undefined must be passed explicitly to get the default value when called.

function increment(step = 1, x: number) :number {
    return x + step
}

increment(undefined.10) / / = > 11
Copy the code

The remaining parameters

ES6 allows us to represent an indefinite number of arguments as an array using residual arguments. We can do this in TypeScript.

function sum(. args:number[]) :number {
    return args.reduce((prev, cur) = > prev + cur)
}

sum(1.2.3) / / = > 6
Copy the code

Note the distinction with arguments objects.

Methods in interfaces

Methods in interfaces can be defined as follows:

interface Animal {
    say(text: string) :void
}

/ / or
interface Animal {
    say: (text: string) = > void
}
Copy the code

The effect of these two annotation methods is the same.

Function overloading

Function overloading allows you to do different things for different arguments and return different data.

Because JavaScript does not support overloading at the language level, we must determine the parameters in the function body to handle them accordingly to simulate function overloading.

function margin(all: number);
function margin(vertical: number, horizontal: number);
function margin(top: number, right: number, bottom: number, left: number);
function margin(a: number, b? :number, c? :number, d? :number) {
    if (b === undefined && c === undefined && d === undefined) {
        b = c = d = a
    } else if (c === undefined && d === undefined) {
        c = a
        d = b
    }

    return {
        top: a,
        right: b,
        bottom: c,
        left: d,
    }
}

console.log(margin(10))
// => {top: 10, right: 10, bottom: 10, left: 10}
console.log(margin(10.20))
// => { top: 10, right: 20, bottom: 10, left: 20 }
console.log(margin(10.20.20.20))
// => { top: 10, right: 20, bottom: 20, left: 20 }
console.log(margin(10.20.20))
// Error
Copy the code

In the example above, the first three declare three function definitions, and the compiler processes the function calls in that order, with the last one being the final function implementation. It is important to note that the last function implementation parameter type must contain all previous parameter type definitions. Therefore, when defining overloads, always put the most precise definition first.

class

Previously, there was no concept of classes in JavaScript, and we used stereotypes to simulate class inheritance until ES6 introduced the class keyword. If you are not familiar with ES6 classes, you are advised to read ECMAScript 6 Primer – Class.

In addition to implementing all the functionality of ES6 classes, TypeScript adds some new uses.

Access modifier

There are three modifiers available in TypeScript: public, private, and protected.

publicThe modifier

Indicates that a property or method is public, accessible within a class, within a subclass, or within an instance of a class. By default, all properties and methods are public.

class Animal {
    public name: string
    
    constructor(name) {
        this.name = name
    }
}
    
let cat = new Animal('Tom')
console.log(cat.name); // => Tom
Copy the code

privateThe modifier

Indicates that an attribute or method is private and can only be accessed within the class.

class Animal {
    private name: string
    
    constructor(name) {
        this.name = name
    }
    
    greet() {
        return `Hello, my name is The ${this.name }. `}}let cat = new Animal('Tom')
console.log(cat.name); // Error: Property "name" is private and can only be accessed in class "Animal".
console.log(cat.greet()) // => Hello, my name is Tom.
Copy the code

protectedThe modifier

Indicates that a property or method is protected, similar to private, but protected properties or methods can also be accessed by their subclasses.

class Animal {
    protected name: string
    
    constructor(name) {
        this.name = name
    }
}
    
class Cat extends Animal {
    constructor(name) {
        super(name)
    }
    
    greet() {
        return `Hello, I'm ${ this.name } the cat.`}}let cat = new Cat('Tom')
console.log(cat.name); // Error: The property "name" is protected and can only be accessed in the class "Animal" and its subclasses.
console.log(cat.greet()) // => Hello, I'm Tom the cat.
Copy the code

Note that TypeScript only does compile-time checking. TS reports an error when you try to access private or protected properties or methods outside the class, but it doesn’t prevent you from accessing those properties or methods.

There is currently a proposal to use the # prefix at the language level to mark an attribute or method as private, see here for those interested.

An abstract class

An abstract class is an abstract representation of a concrete implementation of a class, used as a base class for other classes.

It has two characteristics:

  1. Cannot be instantiated
  2. The abstract methods must be implemented by subclasses

TypeScript uses the abstract keyword to represent abstract classes and their internal abstract methods.

To continue with the Animal example above:

abstract class Animal {
    public abstract makeSound(): void
    public move() {
        console.log('Roaming... ')}}class Cat extends Animal {
    makeSound() {
        console.log('Meow~')}}let tom = new Cat()
tom.makeSound() // => 'Meow~'
tom.move() // => 'Roaming... '
Copy the code

In the example above, we create an abstract class Named Animal that defines an abstract method makeSound. Then, we define a Cat class that inherits from Animal. Since Animal defines the makeSound abstract class, we must implement it in the Cat class. Otherwise, TS will report an error.

// Error: the non-abstract class "Cat" does not implement an abstract member "makeSound" that extends from "Animal".
class Cat extends Animal {
    meow() {
       console.log('Meow~')}}Copy the code

Classes and interfaces

Classes can implement (Implement) interfaces. Through interfaces, you can force classes to comply with a contract. You can declare a method in an interface and then ask the class to implement it.

interface ClockInterface {
    currentTime: Date
    setTime(d: Date)}class Clock implements ClockInterface {
    currentTime: Date
    setTime(d: Date) {
        this.currentTime = d
    }
}
Copy the code

Differences between interfaces and abstract classes

  1. Classes can implement (implement) multiple interfaces, but only extensible (extends) from an abstract class.
  2. Abstract classes can contain concrete implementations, but interfaces cannot.
  3. Abstract classes are visible at run time and pass throughinstanceofJudgment. Interfaces are only useful at compile time.
  4. Interfaces can only describe the common (public), does not check private members, while abstract classes have no such restriction.

summary

This article introduces some important concepts in TypeScript: interfaces, functions, and classes. We learned how to use interfaces to describe object structures, how to describe function types, and how to use classes in TypeScript.