What is the typescript

TypeScript is a free and open source programming language developed by Microsoft. It is a superset of JavaScript, and essentially adds optional static typing and class-based object-oriented programming to the language.

1 TypeScript vs. JavaScript

TypeScript JavaScript
Supersets of JavaScript are used to address the code complexity of large projects A scripting language for creating dynamic web pages
Errors can be found and corrected at compile time As an interpreted language, errors can only be found at run time
Strongly typed, supporting both static and dynamic typing Weak type, no static type option
It is eventually compiled into JavaScript code that the browser can understand It can be used directly in the browser
Support for modules, generics, and interfaces There is no support for modules, generics or interfaces
Community support is still growing, and it’s not great Lots of community support and lots of documentation and problem-solving support

2 for typescript

NPM install -g typescript

Verify the TSC – v

Compile the TSC helloworld. Ts

The base type

Boolean value

let isDone: boolean = false

digital

let decLiteral: number = 6

string

let name: string = "bob"

An array of

let list: number[] = [1, 2, 3]

Let list: Array

= [1, 2, 3];

Tuples Tuple

The tuple type allows you to represent an array with a known number and type of elements that need not be of the same type. For example, you can define a pair of tuples of type string and number.

let x: [string, number];
x = ['hello', 10]; // OK
// Initialize it incorrectly
x = [10, 'hello']; // Error
Copy the code

Enumeration Enum

Enum types complement the JavaScript standard data types

enum Color {Red, Green, Blue}
let c: Color = Color.Green;
Copy the code

By default, elements are numbered from 0. You can also manually specify the values of the members.

enum Color {Red = 1, Green, Blue} let colorName: string = Color[2]; alert(colorName); // display 'Green' because Green is numbered 2 aboveCopy the code

any

Sometimes we want to specify a type for variables whose type is not known at programming time. We don’t want the type checker to check these values and just pass them through compile-time checks. We can then mark these variables with type any:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
Copy the code

void

The void type is like the opposite of any in that it means there is no type. When a function returns no value, you usually see the return type void:

function warnUser(): void {
    alert("This is my warning message");
}
Copy the code

Null and Undefined and the difference

In TypeScript, undefined and null have their own types called undefined and NULL, respectively.

let u: undefined = undefined;

let n: null = null;

To be honest, it makes almost no difference.

The original version of JavaScript made this distinction: NULL is an object representing “nothing” that is zero when converted to a value; Undefined is a primitive value for “none”, which is NaN when converted to a value.

Number(undefined)
// NaN

5 + undefined
// NaN
Copy the code

But such distinctions soon proved infeasible in practice. Currently, null and undefined are basically synonymous with each other, with some subtle differences.

Null means “no object”, meaning there should be no value. Typical usage:

(1) as the parameter of the function, indicating that the parameter of the function is not an object.

(2) as the end of the object prototype chain.

Object.getPrototypeOf(Object.prototype)
// null
Copy the code

Undefined means “missing value”, that is, there should be a value here, but it is not defined yet. Typical usage:

(1) If a variable is declared but not assigned, it is undefined.

(2) When the function is called, the argument that should be provided is not provided, which is equal to undefined.

(3) The object has no assigned attribute. The value of this attribute is undefined.

(4) If the function does not return a value, undefined is returned by default.

var i;
i // undefined

function f(x){console.log(x)}
f() // undefined

var  o = new Object();
o.p // undefined

var x = f();
x // undefined
Copy the code

never

The never type represents the types of values that never exist. For example, the never type is the return type of function expressions or arrow function expressions that always throw an exception or have no return value at all.

The never type is the underlying type in TypeScript. Some examples of how it is naturally allocated:

  • A function that never returns a value (e.g., if it contains while(true) {});

  • A function that always throws an Error(for example: function foo() {throw new Error(‘Not Implemented’)}, foo’s return type is never);

  • You can also use it as a type annotation: let foo: never; // ok

  • However, the never type can only be assigned to another never:

let foo: never = 123; // Error: number cannot be assigned to type never // ok, as the function return type never let bar: never = (() => { throw new Error('Throw my hands in the air like I just dont care'); }) ();Copy the code

Difference from void

When a function returns a null value, it returns a value of type void, but when a function never returns (or always throws an error), it returns a value of type never.

TypeScript assertion

Sometimes you’ll find that you know more about a value than TypeScript does. Usually this happens when you clearly know that an entity has a more exact type than its existing type.

Type assertions are a way of telling the compiler, “Trust me, I know what I’m doing.” Type assertion is like conversion in other languages, but without special data checking and deconstruction. It has no run-time impact, only at compile time.

Type assertions come in two forms:

1. Angle bracket syntax

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
Copy the code

2. As the grammar

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
Copy the code

deconstruction

Deconstruction array

The simplest way to deconstruct an array is to destruct an assignment:

let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
Copy the code

This creates two named variables first and second. Equivalent to using an index, but more convenient:

first = input[0];
second = input[1];
Copy the code

It is better to destruct the declared variable:

[first, second] = [second, first];
Copy the code

On function arguments:

function f([first, second]: [number, number]) {
    console.log(first);
    console.log(second);
}
f(input);
Copy the code

You can use… in arrays. The object expansion operator creates the remaining variable:

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]
Copy the code

Object structure

let person = { name: "Semlinker", gender: "Male", address: "Xiamen", }; // let personWithAge{... Person, age: 33} let {name,... rest } = personCopy the code

Like array deconstruction, you can use undeclared assignments:

({ a, b } = { a: "baz", b: 101 });

The default value

Default allows you to use the default value when the property is undefined:

function keepWholeObject(wholeObject: { a: string, b? : number }) { let { a, b = 1001 } = wholeObject; }Copy the code

Now, keepWholeObject variables a and B have values even if B is undefined.

Function declaration

Destructuring can also be used for function declarations. Here’s a simple case:

type C = { a: string, b? : number } function f({ a, b }: C): void { // ... }Copy the code

However, it’s often more about specifying default values, which can be tricky to deconstruct. First, you need to format the default value before it.

function f({ a, b } = { a: "", b: 0 }): void {
    // ...
}
f(); // ok, default to { a: "", b: 0 }
Copy the code

interface

One of TypeScript’s core tenets is type-checking of the structure a value has. In TypeScript, interfaces name these types and define contracts for your code or third-party code.

Interface is a preliminary study

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

let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);
Copy the code

The type checker looks at calls to printLabel. PrintLabel takes one argument and requires the object argument to have an attribute called label of type string.

Let’s rewrite the above example, this time using an interface: it must contain a label attribute of type string

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
Copy the code

The LabelledValue interface is like a name that describes the requirements in the example above. It represents an object that has a label attribute and is of type String. We’re just going to focus on what the value looks like. As long as the object passed in meets the necessary conditions mentioned above, it is allowed.

It is also worth noting that the type checker does not check the order of attributes, as long as the corresponding attributes exist and the type is correct.

Optional attribute

Not all attributes in the interface are required. Some exist only under certain conditions, or not at all. Optional properties are commonly used in the “Option Bags” mode, where only part of the parameter object passed to the function is assigned a value.

An interface with optional attributes is similar to a normal interface definition, except that the optional attribute name definition is followed by one. Symbols.

interface SquareConfig { color? : string; width? : number; } function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare = {color: "white", area: 100}; if (config.color) { // Error: Property 'clor' does not exist on type 'SquareConfig' newSquare.color = config.clor; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare({color: "black"});Copy the code

One of the benefits of optional attributes is that you can pre-define possible attributes, and another is that you can catch errors when references to non-existent attributes. For example, if we intentionally misspelled the name of the color attribute in createSquare, we would get an error message.

Read-only property

Some object properties can only change their value when the object is created. You can specify read-only properties with readonly before the property name:

interface Point {
    readonly x: number;
    readonly y: number;
}
Copy the code

You can construct a Point by assigning an object literal. After assignment, x and y can never be changed.

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
Copy the code

TypeScript has the ReadonlyArray type, which is similar to Array except that all mutable methods are removed, thus ensuring that arrays can never be modified after they are created:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
Copy the code

On the last line of the code above, you can see that it is not possible to assign the entire ReadonlyArray to a normal array. But you can override it with type assertions:

a = ro as number[];

readonly vs const

The easiest way to determine whether to use readonly or const is to use it as a variable or as a property. Const as a variable, readonly as an attribute.

Additional property checking

We used the interface in the first example. TypeScript tells us to pass in {size: number; label: string; } to just expect {label: string; }.

However, naively combining the two can shoot you in the foot just as it does in JavaScript. For example, take the createSquare example:

interface SquareConfig { color? : string; width? : number; } function createSquare(config: SquareConfig): { color: string; area: number } { // ... } // error: 'colour' not expected in type 'SquareConfig' let mySquare = createSquare({ colour: "red", width: 100 });Copy the code

Note that the parameter passed to createSquare is spelled colour rather than color. In JavaScript, this silently fails.

However, TypeScript considers this code to be buggy. Object literals are treated specially and subject to extra attribute checks when they are assigned to variables or passed as arguments. You get an error if an object literal has any properties that the “target type” does not contain.

Getting around these checks is simple. The easiest way is to use type assertions:

Let mySquare = createSquare({width: 100, opacity: 0.5} as SquareConfig);

However, the best way to do this is to add a string index signature, if you can be sure that the object may have some additional properties for a specific purpose. If SquareConfig had the color and width attributes of the type defined above, and any number of other attributes, we could define it as follows:

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

We’ll talk about index signatures later, but what we’re saying here is that SquareConfig can have any number of attributes, and as long as they’re not color and width, it doesn’t matter what type they are.

One final way to skip these checks, which may surprise you, is to assign this object to another variable: Because squareOptions doesn’t go through additional property checks, the compiler doesn’t report an error.

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
Copy the code

Be aware that in simple code like the one above, you probably shouldn’t circumvent these checks. If you support passing color or colour attributes to createSquare, you should modify the SquareConfig definition to reflect this.

Function types

Interfaces can describe the various shapes that objects in JavaScript can have. In addition to describing ordinary objects with attributes, interfaces can also describe function types.

To use the interface to represent function types, we need to define a call signature for the interface. It is like a function definition with only the argument list and return value types. Each parameter in the parameter list needs a name and type.

interface SearchFunc {
  (source: string, subString: string): boolean;
}
Copy the code

With this definition, we can use this function type interface just like any other interface. The following example shows how to create a variable of function type and assign a function of the same type to it.

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
}
Copy the code

The parameters of the number are checked one by one to ensure that the parameter types in the corresponding positions are compatible. If you don’t want to specify a type, TypeScript’s type system will infer the parameter type because the function is assigned directly to the SearchFunc type variable.

let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}
Copy the code

Indexable type

Much like using interfaces to describe function types, we can also describe types that can be “indexed,” such as a[10] or ageMap[” Daniel “]. Indexable types have an index signature that describes the type of the object index and the corresponding index return value type. Let’s look at an example:

nterface StringArray {
  [index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
Copy the code

In the example above, we define the StringArray interface, which has index signatures. The index signature indicates that when StringArray is indexed by number, it returns a value of type String.

Two index signatures are supported: string and number. Both types of indexes can be used, but the return value of the numeric index must be a subtype of the return value type of the string index. This is because when you index a number, JavaScript converts it to a string and then indexes the object. That is, indexing with 100 (a number) is the same as indexing with 100 (a string), so the two need to be consistent.

class Animal { name: string; } class Dog extends Animal { breed: string; } interface NotOkay { [x: number]: Animal; // error: use 'string' index, sometimes get Animal! [x: string]: Dog; }Copy the code

String index signatures do a good job of describing dictionary patterns, and they also ensure that all attributes match their return value types. Because string indexes declare both obj. Property and obj[“property”]. In the following example, the type of name does not match the type of the string index, so the type checker gives an error:

interface NumberDictionary { [index: string]: number; length: number; Name: string // error, 'name' type does not match index type return value type}Copy the code

Finally, you can set the index signature to read-only, thus preventing index assignment:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
Copy the code

You cannot set myArray[2] because the index signature is read-only.

Class types

Implementing an interface

Like C# or Java interfaces, TypeScript can use them to explicitly force a class to conform to a contract. You can also describe a method in an interface and implement it in a class, as with the following setTime methods:

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

The difference between the static and instance parts of a class

When you work with classes and interfaces, you should know that classes have two types: the type of the static part and the type of the instance. You’ll notice that you get an error when you use the constructor signature to define an interface and try to define a class that implements the interface:

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) { } }Copy the code

This is because when a class implements an interface, only the instance part is type checked. Constructor exists in the static part of the class and is therefore outside the scope of inspection.

Therefore, we should directly manipulate the static part of the class. In the following example, we define two interfaces, ClockConstructor for the constructor and ClockInterface for the instance method. To make it easier to define a constructor, createClock, that creates an instance with the type passed in.

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);
}
class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
  }
}

let digital = createClock(DigitalClock, 12, 17);
Copy the code

This is because when a class implements an interface, only the instance part is type checked. Constructor exists in the static part of the class and is therefore outside the scope of inspection.

Inherited interface

Like classes, interfaces can inherit from each other. This allows us to copy members from one interface to another, giving us more flexibility to split the interface into reusable modules.

interface Shape {
    color: string;
}
interface Square extends Shape {
    sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
Copy the code

An interface can inherit multiple interfaces to create a composite interface of multiple interfaces.

interface Shape { color: string; } interface PenStroke { penWidth: number; } interface Square extends Shape, PenStroke { sideLength: number; } let square = <Square>{}; square.color = "blue"; square.sideLength = 10; Square. PenWidth = 5.0;Copy the code

Mixed type

As we mentioned earlier, interfaces can describe the rich types in JavaScript. Because of the dynamic and flexible nature of JavaScript, sometimes you want an object to have multiple of the types mentioned above.

An example is an object that can be used as both a function and an object, with additional attributes.

interface Counter { (start: number): string; interval: number; reset(): void; } function getCounter(): Counter { let counter = <Counter>function (start: number) { }; counter.interval = 123; counter.reset = function () { }; return counter; } let c = getCounter(); c(10); c.reset(); C.i nterval = 5.0;Copy the code

Interface inheritance class

When an interface inherits a class type, it inherits the members of the class but not its implementation. It is as if an interface declares all the members of a class, but does not provide an implementation. Interfaces also inherit to the private and protected members of the class. This means that when you create an interface that inherits a class with private or protected members, the interface type can only be implemented by that class or its subclasses (Implement).

This is useful when you have a large inheritance structure, but it’s important to point out that your code only works if the subclasses have certain properties. This subclass has nothing to do with the base class except to extend to it. Ex. :

class Control {
    private state: any;
}
interface SelectableControl extends Control {
    select(): void;
}
class Button extends Control implements SelectableControl {
    select() { }
}
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
    select() { }
}
Copy the code

In the example above, the SelectableControl contains all members of Control, including the private member state. Since state is a private member, only subclasses of Control can implement the SelectableControl interface.

Within the Control class, private member States are allowed to be accessed through instances of the SelectableControl. In fact, the SelectableControl acts like Control and has a select method. Button is a subclass of the SelectableControl (because they both inherit from Control and have select methods), but the Image and Location classes are not.

class

class

Here is an example of using a class:

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
let greeter = new Greeter("world");
Copy the code

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

You’ll notice that we use this when referring to any of the class members. It means that we are accessing a member of the class.

In the last line, we construct an instance of the Greeter class using new. It calls the previously defined constructor, creates a new object of Greeter type, and initializes it by executing the constructor.

inheritance

One of the most basic patterns in class-based programming is to allow the use of inheritance to extend existing classes.

class Animal { move(distanceInMeters: number = 0) { console.log(`Animal moved ${distanceInMeters}m.`); } } class Dog extends Animal { bark() { console.log('Woof! Woof! '); } } const dog = new Dog(); dog.bark(); dog.move(10); dog.bark();Copy the code

Here, Dog is a derived class from the Animal base class through the extends keyword. A derived class is usually called a subclass, and a base class is usually called a superclass.

Because Dog inherits Animal functionality, we can create an instance of Dog that can bark() and move().

Now let’s look at a more complicated example.

class Animal { name: string; constructor(theName: string) { this.name = theName; } move(distanceInMeters: number = 0) { console.log(`${this.name} moved ${distanceInMeters}m.`); } } 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); } } let sam = new Snake("Sammy the Python"); let tom: Animal = new Horse("Tommy the Palomino"); sam.move(); // Slithering... // Sammy the Python moved 5m. tom.move(34); // Galloping... // Tommy the Palomino moved 34m.Copy the code

This example demonstrates some features not mentioned above. This time we use the extends keyword to create two subclasses of Animal: Horse and Snake.

Unlike the previous example, the derived class contains a constructor that must call super(), which executes the constructor of the base class. Also, we must call super() before accessing the this property in the constructor. This is an important rule that TypeScript enforces.

This example demonstrates how a subclass can override a parent class’s methods. The Snake class and Horse class both create move methods that override the move method inherited from Animal, so that the move method has different functions depending on the class. Notice that even though Tom is declared as Animal, since its value is Horse, tom.move(34) calls the method overridden in Horse:

Public, private, and protected modifiers

The default for the public

In the example above, we can freely access the members defined in the program. If you’re familiar with classes in other languages, you’ll notice that we didn’t use the public modifier in the previous code; For example, C# requires that members be visible explicitly using public. In TypeScript, members default to public.

You can also explicitly mark a member as public. We can rewrite the Animal class as follows:

class Animal { public name: string; public constructor(theName: string) { this.name = theName; } public move(distanceInMeters: number) { console.log(`${this.name} moved ${distanceInMeters}m.`); }}Copy the code

Understanding of private

When a member is marked private, it cannot be accessed outside the class in which it is declared. Such as:

class Animal { private name: string; constructor(theName: string) { this.name = theName; } } new Animal("Cat").name; // Error: 'name' is private.Copy the code

TypeScript uses the structural type system. When we compare two different types, we don’t care where they came from, if all members’ types are compatible, we assume they are compatible.

However, when we compare types with private or protected members, the situation is different. If one of the types contains a private member, the two types are considered compatible only if there is a private member in the other type and they are both declared from the same place. Use this rule for protected members as well.

Here’s an example to better illustrate the point:

class Animal { private name: string; constructor(theName: string) { this.name = theName; } } class Rhino extends Animal { constructor() { super("Rhino"); } } class Employee { private name: string; constructor(theName: string) { this.name = theName; } } let animal = new Animal("Goat"); let rhino = new Rhino(); let employee = new Employee("Bob"); animal = rhino; animal = employee; // error: Animal is incompatible with Employee.Copy the code

This example has two classes Animal and Rhino, which is a subclass of Animal. There is also an Employee class that looks the same type as Animal. Let’s create several instances of these classes and assign values to each other to see what happens. Animal and Rhino are compatible because they share a private member definition from Animal: String. Employee, however, is not. When assigning Employee to Animal, we get an error stating that their types are incompatible. Although Employee also has a private member name, it is clearly not the one defined in Animal.

Understand the protected

Protected modifiers behave much like private modifiers, with one difference: protected members are still accessible in derived classes. Such as:

class Person { protected name: string; constructor(name: string) { this.name = name; } } class Employee extends Person { private department: string; constructor(name: string, department: string) { super(name) this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; } } let howard = new Employee("Howard", "Sales"); console.log(howard.getElevatorPitch()); console.log(howard.name); / / errorCopy the code

Note that we can’t use name outside of the Person class, but we can still access it through an instance method of the Employee class because Employee is derived from Person.

Constructors can also be marked protected. This means that the class cannot be instantiated outside the containing class, but can be inherited. For instance,

class Person { protected name: string; protected constructor(theName: string) { this.name = theName; Employee extends Person {private department: string; constructor(name: string, department: string) { super(name); this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; } } let howard = new Employee("Howard", "Sales"); let john = new Person("John"); // Error: the constructor for 'Person' is protected.Copy the code

Readonly modifier

You can use the readonly keyword to make the property read-only. Read-only attributes must be initialized at declaration time or in a constructor.

class Octopus { readonly name: string; readonly numberOfLegs: number = 8; constructor (theName: string) { this.name = theName; } } let dad = new Octopus("Man with the 8 strong legs"); dad.name = "Man with the 3-piece suit"; / / error! Name is read-only.Copy the code

Parameter Attributes √

In the example above, we had to define a protected member name and a constructor parameter theName in the Person class and immediately assign values to name and theName. This happens all the time. Parameter properties make it easy to define and initialize a member in one place. The following example is a modification of the previous Animal class, using the parameter attribute:

class Animal { constructor(private name: string) { } move(distanceInMeters: number) { console.log(`${this.name} moved ${distanceInMeters}m.`); }}Copy the code

Notice how we discarded theName and used only the private name: string argument in the constructor to create and initialize theName member. We combine declaration and assignment in one place.

Parameter properties are declared by adding an access qualifier to the constructor parameter. Using private to qualify a parameter property declares and initializes a private member; The same is true for public and protected.

accessor

TypeScript supports intercepting access to object members via getters/setters. It helps you effectively control access to object members.

Here’s how to rewrite a simple class to use get and set. First, let’s start with an example that doesn’t use accessors.

class Employee {
    fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}
Copy the code

We can set fullnames arbitrarily, which is very convenient, but it can also cause trouble.

In this version, we check that the user’s password is correct before allowing him or her to change employee information. We changed direct access to fullName to a set method that checks the password. We also added a get method to make the above example still work.

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!" ); } } } let employee = new Employee(); employee.fullName = "Bob Smith"; if (employee.fullName) { alert(employee.fullName); }Copy the code

We can change the password to verify that the accessor is working. When the password is incorrect, it will prompt us that we have no permission to change the employee.

There are a few things to note about accessors:

First, accessors require that you set the compiler to output ECMAScript 5 or higher. Downgrading to ECMAScript 3 is not supported. Second, accessors with only GET and no set are automatically inferred as readonly. This is helpful when generating.d.ts files from code, because users who exploit this property will see that it is not allowed to change its value.

Static attributes

So far, we’ve only talked about instance members of a class, attributes that are initialized only when the class is instantiated. We can also create static members of a class whose properties exist on the class itself rather than on an instance of the class. In this example, we define Origin as static because it is the property used by all grids. Every instance that wants to access this property must prefix origin with the class name. Just as we use the this. prefix on instance properties to access properties, here we use Grid. To access static properties.

class Grid { static origin = {x: 0, y: 0}; calculateDistanceFromOrigin(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); // 1x scale let grid2 = new Grid(5.0); // 5x scale console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10})); console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));Copy the code

An abstract class

Abstract classes are used as base classes for other derived classes. They are generally not instantiated directly. Unlike interfaces, abstract classes can contain implementation details of members. The abstract keyword is used to define abstract classes and abstract methods within abstract classes.

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}
Copy the code

Abstract methods in abstract classes contain no concrete implementation and must be implemented in derived classes. The syntax of abstract methods is similar to that of interface methods. Both define the method signature but do not contain the method body. However, abstract methods must contain the abstract keyword and can contain access modifiers.

abstract class Department { constructor(public name: string) { } printName(): void { console.log('Department name: ' + this.name); } abstract printMeeting(): void; } class AccountingDepartment extends Department {constructor() {super('Accounting and Auditing'); // You must call super()} printMeeting(): void {console.log('The Accounting Department meets each Monday at 10am.'); } generateReports(): void { console.log('Generating accounting reports... '); } } let department: Department; // Allows you to create a reference to an abstract type department = new department (); // Error: cannot create an instance of an abstract class department = new AccountingDepartment(); // Allow an abstract subclass to be instantiated and assigned department.printName(); department.printMeeting(); department.generateReports(); // Error: method does not exist in declared abstract classCopy the code

Advanced skills √

The constructor

When you declare a class in TypeScript, you’re declaring many things at once. The first is the type of the instance of the class.

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
Copy the code

Here, we write let greeter: greeter, meaning that instances of a class greeter are of type greeter. This is an old habit for programmers who have worked with other object-oriented languages.

We also create a value called a constructor. This function is called when we create an instance of the class using new. Let’s take a look at what the above code looks like when compiled into JavaScript:

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());Copy the code

In the above code, let Greeter will be assigned to the constructor. When we call new and execute this function, we get an instance of the class. This constructor also contains all static attributes of the class. To put it another way, we can think of a class as having an instance part and a static part.

Let’s rewrite the example a bit to see how they differ:

class Greeter { static standardGreeting = "Hello, there"; greeting: string; greet() { if (this.greeting) { return "Hello, " + this.greeting; } else { return Greeter.standardGreeting; } } } let greeter1: Greeter; greeter1 = new Greeter(); console.log(greeter1.greet()); let greeterMaker: typeof Greeter = Greeter; greeterMaker.standardGreeting = "Hey there!" ; let greeter2: Greeter = new greeterMaker(); console.log(greeter2.greet());Copy the code

In this case, Greeter1 is the same as we saw before. We instantiate the Greeter class and use this object. Same thing we saw before.

After that, we use classes directly. We created a variable called greeterMaker. This variable holds the class or holds the class constructor. We then use typeof Greeter, which means take the typeof the class, not the typeof the instance. Or, more accurately, “Tell me the type of the Greeter identifier,” which is the type of the constructor. This type contains all the static members and constructors of the class. Then, as before, we use new on greeterMaker to create an instance of Greeter.

Use classes as interfaces

As we saw in the previous section, a class definition creates two things: the instance type of the class and a constructor. Because classes create types, you can use classes where interfaces are allowed.

class Point {
    x: number;
    y: number;
}
interface Point3d extends Point {
    z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
Copy the code

function

Function types

Define a type for a function

Let’s add a type for the above function:

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

We can add a type for each parameter and then add a return type for the function itself. TypeScript can automatically infer the return value type from the return statement, so we typically omit it.

Inference type

When you try this example, you’ll see that the TypeScript compiler automatically recognizes the type if you specify a type on one side of an assignment but no type on the other:

// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters `x` and `y` have the type number
let myAdd: (baseValue: number, increment: number) => number =
    function(x, y) { return x + y; };
Copy the code

This is called “categorizing by context” and is a type inference. It helps us better type our programs.

Optional and default parameters

Every function argument in TypeScript is required. This does not mean that null or undefined cannot be passed as an argument, but rather that the compiler checks whether the user has passed in a value for each argument. The compiler also assumes that only these arguments are passed into the function. In short, the number of arguments passed to a function must be the same as the number of arguments expected by the function.

function buildName(firstName: string, lastName: string) {
    return firstName + " " + lastName;
}
let result1 = buildName("Bob");                  // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");         // ah, just right
Copy the code

In JavaScript, each parameter is optional and can be passed or not. When no parameter is passed, its value is undefined. In TypeScript we can use it next to parameter names, right? Realize the function of optional parameters. For example, we want last Name to be optional:

function buildName(firstName: string, lastName? : string) { if (lastName) return firstName + " " + lastName; else return firstName; } let result1 = buildName("Bob"); // works correctly now let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters let result3 = buildName("Bob", "Adams"); // ah, just rightCopy the code

Optional arguments must be followed by required arguments. If we wanted first names to be optional in the example above, we would have to reposition them and put first names after them.

In TypeScript, we can also provide a default value for a parameter when the user does not pass the parameter or when the value passed is undefined. They are called parameters with default initialization values. Let’s modify the previous example and set the default value of last name to “Smith”.

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"
let result3 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result4 = buildName("Bob", "Adams");         // ah, just right
Copy the code

Unlike normal optional parameters, parameters with default values do not need to be placed after required parameters. If a parameter with a default value appears before a mandatory parameter, the user must explicitly pass undefined to get the default value. For example, let’s rewrite the last example so that firstName is a parameter with a default value:

function buildName(firstName = "Will", lastName: string) {
    return firstName + " " + lastName;
}

let result1 = buildName("Bob");                  // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr.");  // error, too many parameters
let result3 = buildName("Bob", "Adams");         // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams");     // okay and returns "Will Adams"
Copy the code

The remaining parameters

Required, default, and optional parameters have one thing in common: they represent a parameter. Sometimes, you want to manipulate multiple arguments at once, or you don’t know how many arguments will be passed in. In JavaScript, you can use arguments to access all incoming arguments.

In TypeScript, you can collect all arguments into a single variable:

function buildName(firstName: string, ... restOfName: string[]) { return firstName + " " + restOfName.join(" "); } let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");Copy the code

The remaining parameters are treated as an unlimited number of optional parameters. You could have none, you could have any of them. The compiler creates an array of parameters named after the ellipsis (…) you use. Given a name later, you can use this array inside the body of a function.

The ellipsis is also used on function type definitions with remaining arguments:

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

let buildNameFun: (fname: string, … rest: string[]) => string = buildName;

this

Learning how to use this properly in JavaScript is like a bar mitzvah. Since TypeScript is a superset of JavaScript, TypeScript programmers also need to understand how this works and be able to identify bugs when they occur. Fortunately, TypeScript can tell you where this is being used incorrectly. If you want to understand how this works in JavaScript, first read Understanding JavaScript Function Invocation and “this” by Yehuda Katz. Yehuda’s article explains the inner workings of this in detail, so we will give a brief introduction here.

This and the arrow function

In JavaScript, the value of this is only specified when the function is called. This is a powerful and flexible feature, but you need to spend some time figuring out what the context of the function call is. But as we all know, this is not an easy task, especially when returning a function or passing a function as an argument.

Here’s an example:

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};
        }
    }
}

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

alert("card: " + pickedCard.card + " of " + pickedCard.suit);
Copy the code

You can see that the createCardPicker is a function, and it returns a function. If we try to run this program, we will find that it does not pop up a dialog box but an error. Because this in the function returned by the createCardPicker is set to a window instead of a Deck object. Because we’re just calling cardPicker() independently. Top-level non-method calls treat this as window. (Note: in strict mode, this is undefined, not window).

To solve this problem, we can bind the correct this when the function is returned. That way, no matter how you use it later, you refer to the bound ‘deck’ object. We need to change the function expression to use ECMAScript 6 arrow syntax. The arrow function can hold the value of this when the function was created, not when the function was called:

let deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    createCardPicker: function() {
        // NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);
            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker(); // this= window
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
Copy the code

Even better, TypeScript warns you that you made an error if you set the noImplicitThis flag to the compiler. It will indicate that this in this.suits[pickedSuit] has type any.

This parameter

Unfortunately, this.suits[pickedSuit] still has type any. This is because this comes from the function expression inside the object literal. This can be modified by providing an explicit this parameter. The this argument is a false argument that appears at the top of the argument list:

nterface Card {
    suit: string;
    card: number;
}
interface Deck {
    suits: string[];
    cards: number[];
    createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
    suits: ["hearts", "spades", "clubs", "diamonds"],
    cards: Array(52),
    // NOTE: The function now explicitly specifies that its callee must be of type Deck
    createCardPicker: function(this: Deck) {
        return () => {
            let pickedCard = Math.floor(Math.random() * 52);
            let pickedSuit = Math.floor(pickedCard / 13);

            return {suit: this.suits[pickedSuit], card: pickedCard % 13};
        }
    }
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
Copy the code

Now TypeScript knows that the createCardPicker is expected to be called on a Deck object. That is, this is of type Deck, not any, so –noImplicitThis will not give an error.

The this argument in the callback function

If you pass a function to a library function and it is called later, you’ve probably seen this in the callback return an error. Because when the callback function is called, it will be called as a normal function, this will be undefined. With a few tweaks, you can use this to avoid errors. First, the author of the library function specifies the type of this:

interface UIElement {
    addClickListener(onclick: (this: void, e: Event) => void): void;
}
Copy the code

This: void means that addClickListener expects onclick to be a function and it does not need a this type. Then, add a type annotation for this in the calling code:

class Handler {
    info: string;
    onClickBad(this: Handler, e: Event) {
        // oops, used this here. using this callback would crash at runtime
        this.info = e.message;
    }
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
Copy the code

After specifying this, you explicitly state that onClickBad must be called on an instance of Handler. TypeScript then detects that addClickListener requires the function to have this: void. Change this type to fix this error:

class Handler { info: string; onClickGood(this: void, e: Event) { // can't use this here because it's of type void! console.log('clicked! '); } } let h = new Handler(); uiElement.addClickListener(h.onClickGood);Copy the code

Because onClickGood specifies that this type is void, it is legal to pass addClickListener. Of course, this also means you can’t use this.info. If you want both, you’ll have to use the arrow function:

class Handler {
    info: string;
    onClickGood = (e: Event) => { this.info = e.message }
}
Copy the code

This works because arrow functions don’t capture this, so you can always pass them to functions that expect this: void. The downside is that each Handler object creates an arrow function. Methods, on the other hand, are created only once, adding to the Handler’s prototype chain. They are shared between different Handler objects.

overloading

JavaScript itself is a dynamic language. It is common for JavaScript functions to return different types of data depending on the parameters they pass in.

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;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
Copy the code

The pickCard method returns two different types depending on the argument passed in. If an object representing a card is passed in, the function draws a card from it. If the user wants to draw a card, we tell him what card he caught. But how is that represented in the type system.

The method is to provide multiple function type definitions for the same function for function overloading. The compiler handles function calls based on this list. So let’s reload the pickCard function.

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 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;
    }
    // Otherwise just let them pick the card
    else if (typeof x == "number") {
        let pickedSuit = Math.floor(x / 13);
        return { suit: suits[pickedSuit], card: x % 13 };
    }
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
Copy the code

With this change, the overloaded pickCard function is called with the correct type check.

In order for the compiler to choose the right type of check, it is similar to the processing flow in JavaScript. It looks up the overload list and tries to use the first overload definition. Use this if it matches. Therefore, when defining overloads, always put the most precise definition first.

Note that function pickCard(x): any is not part of the overload list, so there are only two overloads: one to receive objects and one to receive numbers. Calling pickCard with other arguments produces an error.

The generic

In software engineering, it is important not only to create consistent, well-defined apis, but also to consider reusability. The ability of components to support not only current data types but also future data types gives you a lot of flexibility when building large systems.

In languages like C# and Java, generics can be used to create reusable components that can support multiple types of data. This allows users to use components with their own data types.

Generic Hello World

Let’s create our first example of using generics: the identity function. This function returns any value passed to it. You can think of this function as the echo command.

Without generics, the function might look like this:

function identity(arg: number): number {
    return arg;
}
Copy the code

Alternatively, we use any to define functions:

function identity(arg: any): any {
    return arg;
}
Copy the code

Using any causes the function to accept arG arguments of any type, and some information is lost: the type passed in should be the same as the type returned. If we pass in a number, we just know that any type of value can be returned.

Therefore, we need a way to make the type of the return value the same as the type of the parameter passed in. Here, we use a type variable, which is a special variable that only represents a type, not a value.

function identity<T>(arg: T): T {
    return arg;
}
Copy the code

We add the type variable T to identity. T helps us capture the type passed in by the user (such as: number), which we can then use. Then we used T again as the return type. Now we know that the parameter type is the same as the return value type. This allows us to track information about the types used in functions.

We call this version of the Identity function generic because it can be applied to multiple types. Unlike using any, it does not lose information, as in the first example, which maintains accuracy, passing in the numeric type and returning the numeric type.

Once we have defined a generic function, we can use it in two ways. The first is to pass in all the arguments, including the type arguments:

let output = identity<string>("myString"); // type of output will be 'string'

Here we explicitly specify that T is a string and is passed to the function as an argument, enclosed in <> instead of ().

The second method is more common. Makes use of type corollaries — that is, the compiler automatically helps us determine the type of T based on the arguments passed in:

let output = identity("myString"); // type of output will be 'string'

Note that we do not need to use Angle brackets (<>) to pass in the type explicitly; The compiler can look at the value of myString and set T to its type. Type inference helps us keep our code lean and readable. If the compiler cannot automatically infer the type, it can only pass in the type of T explicitly as above, which is possible in some complicated cases.

Use generic variables

When creating a generic function like Identity using a generic type, the compiler requires that you use the generic type correctly in the function body. In other words, you must treat these parameters as any or all types.

If we want to print out the length of arg at the same time. We would probably do something like this:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
    }
Copy the code

If we do this, the compiler will say we are using the.length attribute of the ARG, but there is no place to indicate that the ARG has this attribute. Remember, these type variables represent arbitrary types, so someone using this function could pass in a number that doesn’t have a.length attribute.

Now suppose we want to operate on an array of type T instead of T directly. Since we’re dealing with arrays, the.length property should be there. We can create this array just like any other array:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}
Copy the code

You can think of the type of loggingIdentity like this: the generic function loggingIdentity takes the type parameter T and the parameter arg, which is an array of element type T, and returns an array of element type T. If we pass in an array of numbers, we return an array of numbers, because T is of type number. This allows us to use the generic variable T as part of the type, rather than the whole type, adding flexibility.

We could also implement the above example like this:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}
Copy the code

If you’ve spoken other languages, you’re probably already familiar with this syntax. In the next section, you’ll see how to create custom generics like Array.

The generic type

The type of a generic function is no different from that of a non-generic function, except that there is a type parameter first, like a function declaration:

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
Copy the code

We can also use different generic parameter names, as long as they match in number and usage.

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
Copy the code

This led us to write our first generic interface. Let’s take the object literal from the above example as an interface:

interface GenericIdentityFn {
    <T>(arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;
Copy the code

As a similar example, we might want to treat a generic parameter as a parameter to the entire interface. This way we know exactly which generic type to use (e.g., Dictionary, not just Dictionary). This allows other members of the interface to know the type of the parameter.

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
Copy the code

Note that our example has changed a little. Instead of describing generic functions, nongeneric function signatures are used as part of generic types. When we use GenericIdentityFn, we also pass in a type parameter to specify the generic type (in this case: number), locking the type to be used later in the code. Understanding when to put parameters in the call signature and when to put parameters on the interface is helpful in describing which part of a type is a generic part.

A generic class

Generic classes look much like generic interfaces. Generic classes use (<>) to enclose generic types after the class name.

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
Copy the code

The GenericNumber class is fairly straightforward to use, and as you may have noticed, there is nothing limiting it to the number type. You can also use strings or other more complex types.

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
Copy the code

As with interfaces, placing generic types directly after classes helps us ensure that all attributes of a class are using the same type.

As we said in the class section, a class has two parts: a static part and an instance part. A generic class refers to the type of the instance part, so static attributes of the class cannot use this generic type.

Generic constraint

You’ll remember from an earlier example where we sometimes wanted to manipulate a set of values of a certain type, and we knew what properties that set of values had. In the loggingIdentity example, we want to access the LENGTH attribute of the ARG, but the compiler can’t prove that every type has the length attribute, so we get an error.

To do this, we need to list the constraints on T and define an interface to describe the constraints. Create an interface that contains the. Length attribute and implement constraints using this interface and the extends keyword:

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}
Copy the code

Now the generic function is constrained, so it no longer applies to any type:

loggingIdentity(3); // Error, number doesn't have a .length property

We need to pass in values that match the constraint type and must contain the required attributes:

loggingIdentity({length: 10, value: 3});

Use type parameters in generic constraints

You can declare one type parameter and it is bound by another type parameter. For example, now we want to get the property from the object using the property name. And we want to make sure that this property exists on the object obj, so we need to use constraints between the two types.

function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
Copy the code

Use class types in generics

When TypeScript uses generics to create factory functions, you need to reference the class type of the constructor. For instance,

function create<T>(c: {new(): T; }): T {
    return new c();
}
Copy the code

A more advanced example uses stereotype attributes to infer and constrain the relationship of a constructor to a class instance.

class BeeKeeper {
    hasMask: boolean;
}
class ZooKeeper {
    nametag: string;
}
class Animal {
    numLegs: number;
}
class Bee extends Animal {
    keeper: BeeKeeper;
}
class Lion extends Animal {
    keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
    return new c();
}
createInstance(Lion).keeper.nametag;  // typechecks!
createInstance(Bee).keeper.hasMask;   // typechecks!
Copy the code

Symbols

The value of the symbol type is created through the Symbol constructor.

let sym1 = Symbol(); let sym2 = Symbol("key"); // The optional string keyCopy the code

The Symbols are immutable and unique.

let sym2 = Symbol("key");
let sym3 = Symbol("key");
Copy the code

sym2 === sym3; // False, the symbols are the only keys that can be used for object properties, just like strings.

let sym = Symbol();
let obj = {
    [sym]: "value"
};
console.log(obj[sym]); // "value"
Copy the code

Symbols can also be combined with computed property name declarations to declare attributes and class members of an object.

const getClassNameSymbol = Symbol();
class C {
    [getClassNameSymbol](){
       return "C";
    }
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"
Copy the code

In addition to the user defined symbols, there are several built-in symbols that are already well known. The built-in symbols are used to represent behavior within the language.

Here is a list of these symbols:

The symbol. hasInstance method, which is called by the instanceof operator. The constructor object is used to identify whether an object is an instance.

Symbol. IsConcatSpreadable Boolean value, said while calling on an object Array. The prototype. Concat, whether the object Array element can be carried out.

Iterator method, called by the for-of statement. Returns the default iterator for the object.

Symbol. Match method, called by string.prototype. match. Regular expressions are used to match strings.

The symbol.replace method, called by string.prototype. replace. Regular expressions are used to replace matched substrings in a string.

The symbol. search method, called by string.prototype. search. The regular expression returns the index of the matched portion in the string.

Symbol. Species function value, which is a constructor. Used to create derived objects.

The symbol. split method, called by string.prototype. split. Regular expressions to split strings.

Symbol. ToPrimitive method, called by the toPrimitive abstract operation. Converts an object to its corresponding original value.

Method is built-in Symbol. ToStringTag Object. The prototype. ToString calls. Returns the default string description when the object is created.

Symbol. Unscopables objects whose own properties are excluded from the with scope.

referenceTs Official Documentation