Class types – Efficient use of typed object-oriented programming tools

Set object-oriented abstract, encapsulation, polymorphism three elements as one programming tool, class type.

In JavaScript (ES5), only the implementation of simulated classes (used to abstract business models, organize data structures, and create reusable components) through function and prototype inheritance was supported, and only after the class keyword was introduced in ES6 did it begin to support the definition of declared classes using a java-like syntax.

6.1 class

Any entity can be abstracted into an object-like data structure expressed using a class that contains both properties and methods.

To define a class using traditional JavaScript code, we need to simulate it using a chain of functions + prototypes, as shown in the following code:

function Dog(name: string) {
  this.name = name; // ts(2683) 'this' implicitly has type 'any' because it does not have a type annotation.
}
Dog.prototype.bark = function () {
  console.log('Woof! Woof! ');
};
​
const dog = new Dog('Q'); // ts(7009) 'new' expression, whose target lacks a construct signature, implicitly has an 'any' type.
dog.bark(); // => 'Woof! Woof! '
Copy the code

This approach is significantly more cumbersome than defining classes in the class way, and also lacks static type checking.

6.2 inheritance

Using the extends keyword makes it easy to define abstract patterns that classes inherit

class Animal {
  type = 'Animal';
  say(name: string) {
    console.log(`I'm ${name}! `); }}class Dog extends Animal {
  bark() {
    console.log('Woof! Woof! '); }}const dog = new Dog();
dog.bark(); // => 'Woof! Woof! '
dog.say('Q'); // => I'm Q!
dog.type; // => Animal
Copy the code

Note: Derived classes are often called subclasses, and base classes are also called superclasses (or superclasses).

A derived class that contains a constructor must call the super() method in that constructor, an important rule that TypeScript enforces. As follows:

class Dog extends Animal {
  name: string;
  constructor(name: string) { // ts(2377) Constructors for derived classes must contain a 'super' call.
    this.name = name;
  }

  bark() {
    console.log('Woof! Woof! '); }}class Dog extends Animal {
  name: string;
  constructor(name: string) {
    super(a);// Add the super method
    this.name = name;
  }

  bark() {
    console.log('Woof! Woof! '); }}Copy the code

The super function here calls the constructor of the base class

class Animal {
  weight: number;
  type = 'Animal';
  constructor(weight: number) {
    this.weight = weight;
  }
  say(name: string) {
    console.log(`I'm ${name}! `); }}class Dog extends Animal {
  name: string;
  constructor(name: string) {
    super(a);// ts(2554) Expected 1 arguments, but got 0.
    this.name = name;
  }

  bark() {
    console.log('Woof! Woof! '); }}Copy the code

Moving the mouse over the super function called by the Dog class constructor on line 15, we see a hint that its type is the constructor of the base class Animal: constructor Animal(weight: number): Animal. And because the Animal constructor requires that you pass a weight parameter of numeric type, and line 15 actually takes an empty parameter, an error ts(2554) is displayed; If we explicitly pass the super function a value of type number, such as super(20), we no longer get an error.

6.3 Public, Private, and Protected modifiers

In addition to being inherited through extends, class properties and methods can also control accessibility through modifiers.

TypeScript supports three types of access modifiers: public, private, and protected.

  • Public refers to a property or method that is visible and public anywhere;

  • Private refers to properties or methods that are only visible and private within the same class;

  • Protected refers to a property or method that is visible and protected only within the class itself and subclasses.

In the previous code, the example class did not use the visibility modifier. By default, the properties or methods of the class are public. If we want to make some properties invisible to the outside world, we can use private to set them as follows:

class Son {
  public firstName: string;
  private lastName: string = 'Stark';
  constructor(firstName: string) {
    this.firstName = firstName;
    this.lastName; // ok}}const son = new Son('Tony');
console.log(son.firstName); // => "Tony"
son.firstName = 'Jack';
console.log(son.firstName); // => "Jack"
console.log(son.lastName); // ts(2341) Property 'lastName' is private and only accessible within class 'Son'.
Copy the code

Note: Private properties of classes defined in TypeScript only represent private at the static type detection level. If we force the TypeScript type to be ignored for checking errors, we can still get the lastName property when we translate and run JavaScript, because JavaScript does not support true private properties.

class Son {
  public firstName: string;
  protected lastName: string = 'Stark';
  constructor(firstName: string) {
    this.firstName = firstName;
    this.lastName; // ok}}class GrandSon extends Son {
  constructor(firstName: string) {
    super(firstName);
  }

  public getMyLastName() {
    return this.lastName; }}const grandSon = new GrandSon('Tony');
console.log(grandSon.getMyLastName()); // => "Stark"
grandSon.lastName; // ts(2445) Property 'lastName' is protected and only accessible within class 'Son' and its subclasses.
Copy the code

In line 3, change the lastName attribute visibility modifier on the Son class to protected, indicating that this attribute is visible in the Son class and its subclasses. As shown in lines 6 and 16 of the example, we can get the value of the lastName property from the constructor of the Son parent class and from the getMyLastName method of Son’s GrandSon child, GrandSon.

Note: Although protected modified properties and methods cannot be accessed through an instance of the derived class, they can be accessed through an instance method of the derived class. For example, in line 21 of the example, getting the protected property lastName through the instance’s getMyLastName method is OK, while line 22 getting the protected property lastName directly through the instance prompts an error of TS (2445).

6.4 Read-only modifier

If we don’t want a property of a class to be changed, we can declare a property of a class using the readonly modifier

class Son {
  public readonly firstName: string;
  constructor(firstName: string) {
    this.firstName = firstName; }}const son = new Son('Tony');
son.firstName = 'Jack'; // ts(2540) Cannot assign to 'firstName' because it is a read-only property.
Copy the code

Note: If the read-only modifier and the visibility modifier appear together, we need to write the read-only modifier after the visibility modifier.

6.5 accessor

Getters and setters are also used in TypeScript to intercept read and write access to class members.

By intercepting access to class attributes, we can implement some specific access control logic.

class Son {
  public firstName: string;
  protected lastName: string = 'Stark';
  constructor(firstName: string) {
    this.firstName = firstName; }}class GrandSon extends Son {
  constructor(firstName: string) {
    super(firstName);
  }
  get myLastName() {
    return this.lastName;
  }
  set myLastName(name: string) {
    if (this.firstName === 'Tony') {
      this.lastName = name;
    } else {
      console.error('Unable to change myLastName'); }}}const grandSon = new GrandSon('Tony');
console.log(grandSon.myLastName); // => "Stark"
grandSon.myLastName = 'Rogers';
console.log(grandSon.myLastName); // => "Rogers"
const grandSon1 = new GrandSon('Tony1');
grandSon1.myLastName = 'Rogers'; // => "Unable to change myLastName"
Copy the code

6.6 Static Properties

All of the properties and methods described above are initialized only when the class is instantiated. In fact, we can also define static properties and methods for classes.

Since these properties exist on the particular object of the class, not on the instance of the class, we can access the static properties directly through the class, as shown in the following code:

class MyArray {
  static displayName = 'MyArray';
  static isArray(obj: unknown) {
    return Object.prototype.toString.call(obj).slice(8, -1) = = ='Array'; }}console.log(MyArray.displayName); // => "MyArray"
console.log(MyArray.isArray([])); // => true
console.log(MyArray.isArray({})); // => false
Copy the code

With the static modifier, we define a static property, displayName, and a static method, isArray, for the MyArray class. After that, we can directly access static properties and methods on the class without having to instantiate MyArray

Based on the static properties feature, we tend to define class-related constants, properties and methods that do not depend on the instance this context as static properties to avoid data redundancy and improve performance.

Note that methods that do not depend on the instance this context can be defined as static methods. This means that you need to explicitly annotate this to use this in static methods. Non-static methods do not need to explicitly annotate the type this, because the default point to this is an instance of the class.

6.7 the abstract class

It is a special class that cannot be instantiated and can only be inherited by subclasses.

We can use an abstract class to define the properties and methods that the derived class implements. We can also define other inherited default properties and methods, as shown in the following code:

abstract class Adder {
  abstract x: number;
  abstract y: number;
  abstract add(): number;
  displayName = 'Adder';
  addTwice(): number {
    return (this.x + this.y) * 2; }}class NumAdder extends Adder {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    super(a);this.x = x;
    this.y = y;
  }
  add(): number {
    return this.x + this.y; }}const numAdder = new NumAdder(1.2);
console.log(numAdder.displayName); // => "Adder"
console.log(numAdder.add()); / / = > 3
console.log(numAdder.addTwice()); / / = > 6
Copy the code

With the abstract keyword, we define an abstract class Adder. With the abstract keyword, we define abstract properties x, y, and methods add. Any class derived from Adder needs to implement these abstract properties and methods.

If the derived class lacks an implementation for any of the abstract members of x, y, or add, then line 12 prompts a TS (2515) error, which you can verify for yourself.

Other non-abstract members of the abstract class can be obtained directly from the instance. For example, in lines 26 to 28, we get the displayName property and the addTwice method from the instance numAdder.

Because an abstract class cannot be instantiated, and a derived class must implement the abstract properties and method definitions inherited from the abstract class, the function of an abstract class is to encapsulate and abstract the underlying logic.

In fact, we can also define an interface type that describes the structure of an object (see section 07). The structure of an abstract class and constrain the implementation of the class with the implements keyword.

The difference between using an interface and using an abstract class is that an interface can only define the types of class members

interface IAdder {
  x: number;
  y: number;
  add: () = > number;
}
class NumAdder implements IAdder {
  x: number;
  y: number;
  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
  add() {
    return this.x + this.y;
  }
  addTwice() {
    return (this.x + this.y) * 2; }}Copy the code

Type of class 6.8

The last feature of a class is that its type is similar to a function. When you declare a class, you also declare a special type (specifically, an interface type). The name of the type is the class name, indicating the type of the instance of the class. When we define a class, we declare the types of all properties and methods except constructors as members of this particular type.

class A {
  name: string;
  constructor(name: string) {
    this.name = name; }}const a1: A = {}; // ts(2741) Property 'name' is missing in type '{}' but required in type 'A'.
const a2: A = { name: 'a2' }; // ok
Copy the code

When we define class A, we also define an interface type A of the same name that contains the string attribute name. Therefore, when an empty object is assigned to A variable a1 of type A on line 7, TypeScript prompts A TS (2741) error because the name attribute is missing. When an object {name: ‘a2’} is assigned to A variable a2 of type A on line 8, TypeScript passes type checking directly because of the name attribute.

In TypeScript, classes are essential because we need to practice OOP programming ideas. In practice, classes, like functions, are extremely useful abstractions and wrappers.

7 Interface type and type alias

In this installment, we’ll look at TypeScript’s unique but essential features — interface types and type aliases. These features give TypeScript the ability to describe more complex data structures that JavaScript lacks. Before TypeScript, you might have been able to do this only with documentation or lots of comments.

7.1Interface Interface type

TypeScript not only helps change the way the front end thinks, it also reinforces the thinking and ability to program to interfaces, thanks to the Interface type. Interface types allow us to clearly define communication rules within modules, across modules, and across project code.

TypeScript type detection of objects follows a principle known as duck typing or structural subtyping: as long as two objects have the same structure and the same types of properties and methods, they are of the same type.

function Study(language: { name: string; age: () => number }) {
  console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`);
}
Study({
  name: 'TypeScript'.age: () = > new Date().getFullYear() - 2012
});
Copy the code

During a function call, the TypeScript static type detects that the passed object’s name property of type string and age property of type () => number are of the same type as the function’s arguments, and does not throw a type error.

If we pass in an object literal whose name property is of type number or whose age property is missing,

Study({
  name: 2.age: () = > new Date().getFullYear() - 2012
});
Study({
  name: 'TypeScript'
});
Copy the code

Error: ts(2322) number cannot be assigned to string on line 2. Error: ts(2345) Argument is incompatible with the Parameter type and lacks the required property age.

Similarly, if we pass an object literal that contains an ID property that is not in the Parameter type definition, we also get a type error ts(2345), the Argument is incompatible with the Parameter type, and the property ID does not exist, as shown below:

/** ts(2345) The Argument is incompatible with the Parameter type, and the attribute id */ does not exist
Study({
  id: 2.name: 'TypeScript'.age: () = > new Date().getFullYear() - 2012
});
Copy the code

Interestingly, in the example above, if we first assigned the object literal to a variable and then passed the variable to a function to call, TypeScript static type detection would detect only the type of the property defined in the parameter type and implicitly ignore any redundant properties. An error of type TS (2345) will not be thrown.

let ts = {
  id: 2.name: 'TypeScript'.age: () = > new Date().getFullYear() - 2012
};
Study(ts); // ok
Copy the code

This is not an omission or bug, but rather a deliberate separation of object literals from variables, which we call object literals freshness (more on this again in Lecture 12).

Because this inline form of interface type definition is syntactically similar to familiar JavaScript deconstruction, it’s easy to get confused. Here’s an example to compare the effects of deconstruction syntax mixed with inline interface types.

/** Pure JavaScript destruct syntax */
function StudyJavaScript({name, age}) {
  console.log(name, age);
}
/** iN TypeScript, deconstruction is mixed with inline types
function StudyTypeScript({name, age}: {name: string, age: () => number}) {
    console.log(name, age);
}
/** Pure JavaScript destruct syntax, define alias */
function StudyJavaScript({name: aliasName}) { // Define an alias for name
  console.log(aliasName);
}
/** TypeScript */
function StudyTypeScript(language: {name: string}) {
  // console.log(name); // You cannot print the name directly
  console.log(language.name);  
}
Copy the code

In functions, the syntax for object destructuring and defining interface types is similar (as shown in lines 12 and 17), so be careful not to confuse them. In fact, the interface types defined inline are not reusable, so we should use the interface keyword more to pull out reusable interface types.

/ ** keyword interface name */ interface ProgramLanguage {/** Language name */
  name: string;
  /** Service life */
  age: () = > number;
}
Copy the code

The syntax format of an interface is the space of the interface keyword + the name of the interface, and the definitions of attributes and attribute types are enclosed in parentheses.

function NewStudy(language: ProgramLanguage) {
  console.log(`ProgramLanguage ${language.name} created ${language.age()} years ago.`);
}
Copy the code

We can also constrain other logic by reusing interface type definitions. For example, we define a variable TypeScript of type ProgramLanguage with the code shown below.

let TypeScript: ProgramLanguage;
Copy the code

We then assign an object literal that satisfies the interface type convention to the variable without reporting an error.

TypeScript = {
  name: 'TypeScript'.age: () = > new Date().getFullYear() - 2012
}
Copy the code

Any non-convention will prompt a type error. An additional attribute ID in the following example that is not defined by the interface also prompts a TS (2322) error: Object literals cannot be assigned to a TypeScript variable of type ProgramLanguage.

TypeScript = {
  name: 'TypeScript'.age: () = > new Date().getFullYear() - 2012.id: 1
}
Copy the code

7.2 Default properties

If we want object literals that lack the age attribute to conform to the convention and not throw a type error, specifically the age attribute can default in the interface type, then we can follow the property name by adding? Syntax to annotate properties or methods that can default.

/** Interface name */
interface OptionalProgramLanguage {
  /** Language name */
  name: string;
  /** Service life */age? :() = > number;
}
let OptionalTypeScript: OptionalProgramLanguage = {
  name: 'TypeScript'
}; // ok
Copy the code

When a property is marked as default, its type becomes a union of the explicitly specified type and the undefined type. For example, the OptionalTypeScript age property type becomes something like this:

(() = > number) | undefined;
Copy the code

Think outside the box: Do you think OptionalTypeScript2 is equivalent to OptionalTypeScript?

/** Interface name */
interface OptionalProgramLanguage2 {
  /** Language name */
  name: string;
  /** Service life */
  age: (() = > number) | undefined;
}
Copy the code

The answer, of course, is not equivalent, just like we said in lecture 05 that a function can have default arguments and that the parameter type can be undefined, which means that the property key name can’t be set, and undefined means that the property key name can’t be default.

Since the value may be undefined, if we need to operate on the object’s properties or methods, we can use the type guard (see section 11) or the Optional Chain (add? After the property name on line 5). , as shown in the following code:

if (typeof OptionalTypeScript.age === 'function') { OptionalTypeScript.age(); } OptionalTypeScript.age? . ();Copy the code

7.3 Read-only Attribute

We can make a name a read-only property by adding the readonly modifier before the property name.

interface ReadOnlyProgramLanguage {
  /** Language name */
  readonly name: string;
  /** Service life */
  readonly age: (() = > number) | undefined;
}
 
let ReadOnlyTypeScript: ReadOnlyProgramLanguage = {
  name: 'TypeScript'.age: undefined
}
/** ts(2540) error, name read only */
ReadOnlyTypeScript.name = 'JavaScript';
Copy the code

Note that this is only read-only at the static type detection level and does not actually prevent tampering with the object. This is because the readonly modifier is erased after translation to JavaScript. Therefore, it is safer practice to return a new object 👍 any time rather than modifying an object directly.

7.4 Defining function Types

Interface types can be used to define not only the types of objects, but also the types of functions (note: only the types of functions are defined, not the implementation of the functions).

interface StudyLanguage {
  (language: ProgramLanguage): void
}
/** Separate function practices */
let StudyInterface: StudyLanguage 
  = language= > console.log(`${language.name} ${language.age()}`);
Copy the code

In practice, we rarely use interface types to define function types. We use inline types or type aliases (later in this lecture) in conjunction with arrow function syntax to define function types, as shown in the following example:

type StudyLanguageType = (language: ProgramLanguage) = > void
Copy the code

We specify an alias StudyLanguageType for the arrow function type, so we can reuse StudyLanguageType elsewhere without having to declare a new arrow function type definition.

7.5 Index Signature

In actual work, objects are used more frequently, such as Props & State of React component and Props of HTMLElement. These objects have a common feature, that is, all attribute names and method names are determined.

In fact, we often use objects as maps, such as the code example below which defines object LanguageRankMap with an index of any number and object LanguageMap with an index of any string.

let LanguageRankMap = {
  1: 'TypeScript'.2: 'JavaScript'. };let LanguageMap = {
  TypeScript: 2012.JavaScript: 1995. };Copy the code

At this point, we need to use the index signature to define the object mapping structure mentioned above, and to constrain the type of the index by the format “[index name: type]”.

The types of index names are string and Number. The LanguageRankInterface and LanguageYearInterface defined below can be used to describe an index as an object of any number or string.

interface LanguageRankInterface {
  [rank: number]: string;
}
interface LanguageYearInterface {
  [name: string]: number;
}
{
  let LanguageRankMap: LanguageRankInterface = {
    1: 'TypeScript'.// ok
    2: 'JavaScript'.// ok
    'WrongINdex': '2012' // ts(2322) Nonexistent attribute name
  };
  
  let LanguageMap: LanguageYearInterface = {
    TypeScript: 2012.// ok
    JavaScript: 1995.// ok
    1: 1970 // ok
  };
}
Copy the code

Note: In the above example, when a number is used as an object index, its type can be compatible with either numbers or strings, which is consistent with JavaScript behavior. Therefore, when indexing objects with 0 or ‘0’, the two are equivalent.

Similarly, we can annotate the index signature using the readonly annotation, in which case the corresponding property is set to read-only

{ interface LanguageRankInterface { readonly [rank: number]: string; } interface LanguageYearInterface { readonly [name: string]: number; }}Copy the code

Note: Although an attribute can be mixed with an index signature, the type of the attribute must be a subset of the type of the corresponding numeric or string index, otherwise an error message will appear.

{
  interface StringMap {
    [prop: string]: number;
    age: number; // ok
    name: string; // the string type of the ts(2411) name attribute cannot be assigned to the string index type number
  }
  interface NumberMap {
    [rank: number]: string;
    1: string; // ok
    0: number; // The ts(2412) 0 attribute of type number cannot be assigned to the numeric index type string
  }
  interface LanguageRankInterface {
    name: string; // ok
    0: number; // ok[rank: number]: string; [name: string]: number; }}Copy the code

Because the type string of the interface StringMap property name is not a subset of the type number of its corresponding string index (prop: string defined in line 3), an error is displayed. Similarly, the interface NumberMap attribute 0 has an error because the type number is not a subset of the type string of its corresponding numeric index (rank: number, defined on line 8).

In addition, because of the special nature of numeric type indexes mentioned above, we cannot restrict numeric indexed properties to have distinct types from string indexed properties

{
  interface LanguageRankInterface {
    [rank: number]: string; // ts(2413) String type cannot be assigned to string index type number[prop: string]: number; }}Copy the code

The rank of the LanguageRankInterface is incompatible with the string index prop of type number, so a TS (2413) error is displayed.

Here’s a hint: If we really need to use an object data structure with age of type Number and other attribute types of String, how do we define its type without prompting an error?

For example, how do we define an object whose age attribute is a number and any other attribute is a string?


{
  age: 1.// Numeric type
  anyProperty: 'str'./ / string. }Copy the code

What do we do when we cannot describe the object through a single interface due to the type limitations of the properties and index signatures? And we’ll solve that in lecture 08.

7.6 Inheritance and Implementation

In TypeScript, interface types can inherit and be inherited. For example, we can use the extends keyword as shown below to implement interface inheritance.

{
  interface DynamicLanguage extends ProgramLanguage {
    rank: number; // Define new attributes
  }
  
  interface TypeSafeLanguage extends ProgramLanguage {
    typeChecker: string; // Define new attributes
  }
  /** inherit multiple */
  interface TypeScriptLanguage extends DynamicLanguage, TypeSafeLanguage {
    name: 'TypeScript'; // Redefines an attribute with a compatible type (such as a subset) of the original attribute type}}Copy the code

Note: We can only override inherited properties with compatible types

{
  /** ts(6196) error inheritance, name attribute incompatible */
  interface WrongTypeLanguage extends ProgramLanguage {
    name: number; }}Copy the code

We can use interface types to constrain classes, or we can use classes to implement interfaces. What is the relationship between the two? Here, we describe the relationship between the class and the interface by using the implements keyword as shown below.

/** class implements interface */
{
  class LanguageClass implements ProgramLanguage {
    name: string = ' ';
    age = () = > new Date().getFullYear() - 2012}}Copy the code

7.7Type Type alias

One of the functions of interface types is to pull out inline types, thus enabling type reuse. In fact, we can also use type aliases to receive extracted inline types for reuse.

At this point, we can define the type alias in the format “type alias name = type definition” as follows.

/** Type alias */
{
  type LanguageType = {
    /** The following are interface attributes */
    /** Language name */
    name: string;
    /** Service life */
    age: () = >number; }}Copy the code

In addition, in cases where interface types cannot be overridden, such as composite types and cross types (see section 08), we can only use type aliases, as shown in this code:

{
  /** 联合 */
  type MixedType = string | number;
  Cross / * * * /
  type IntersectionType = { id: number; name: string; } and {age: number; name: string };
  /** Extract the interface attribute type */
  type AgeType = ProgramLanguage['age'];  
}
Copy the code

We define an alias for IntersectionType, which represents the type intersected by two anonymous interface types. An AgeType alias is also defined to represent the type of the extracted ProgramLanguage Age attribute.

7.8 Differences between Interface and Type

Where interface type annotations are applicable, they can be replaced with type aliases. Does this mean that the two are equivalent in the appropriate scenario?

In fact, using interface types and type aliases has the same effect in most cases, but there are some specific scenarios in which the two are quite different. For example, if an interface type is repeatedly defined, its properties will overlap. This feature makes it extremely easy to extend global variables and third-party library types, as shown in the following code:

{
  interface Language {
    id: number;
  }
  
  interface Language {
    name: string;
  }
  let lang: Language = {
    id: 1.// ok
    name: 'name' // ok}}Copy the code

The two Language “interface” attributes defined in sequence are superimposed, and we can assign the lang variable an object containing both the ID and name attributes.

However, if we repeat the definition of the type alias, as shown in the following code, we get a TS (2300) error.

{
  /** ts(2300) duplicate mark */
  type Language = {
    id: number;
  }
  
  /** ts(2300) duplicate mark */
  type Language = {
    name: string;
  }
  let lang: Language = {
    id: 1.name: 'name'}}Copy the code

Interface types are one of the most important aspects of TypeScript. Mastering interface types and developing an interface oriented programming mindset will make programming easier and more efficient.

Type aliasing allows a type to be assigned to another variable (alias) just like a value, which greatly improves type reusability and ultimately improves programming efficiency.

8 Advanced types: union types and crossover types

8.1 Union Types

The type of a variable or parameter is used to represent that it is not a single atomic type, but may be a combination of many different types.

We mainly through “|” operator separated type to represent the joint type of grammar. Here, we can put the “|” analogy logic in JavaScript or “| |”, said the former would just type.

function formatPX(size: unknown) {
  if (typeof size === 'number') {
    return `${size}px`;
  }
  if (typeof size === 'string') {
    return `The ${parseInt(size) || 0}px`;
  }
  throw Error('Only number or string' is supported);
}
formatPX(13);
formatPX('13px');
Copy the code

Note: Before we learn about union types, we can’t avoid using any or unknown types for parameter types (unknown is recommended for good habits).

The problem with this approach is that we can pass arbitrary values when calling formatPX, which can be statically typed (as is the case with any), but the runtime still throws an error, such as:

formatPX(true);
formatPX(null);
Copy the code

This is clearly not what we expected, because size should be more explicit, that is, it could and could only be the type of the two optional types, number or String.

Thanks to the union type, we can annotate the size parameter with a more explicit union type that could be number or string, as shown in this code:

function formatPX(size: number | string) {
  // ...
}
formatPX(13); // ok
formatPX('13px'); // ok
formatPX(true); / / ts (2345) 'true' type cannot give 'number | string' type
formatPX(null); / / ts (2345) 'null' type cannot give 'number | string' type
Copy the code

Of course, we can combine any type, any type, to create a type that better suits our needs.

function formatUnit(size: number | string, unit: 'px' | 'em' | 'rem' | The '%' = 'px') {
  // ...
}
formatUnit(1.'em'); // ok
formatUnit('1px'.'rem'); // ok
formatUnit('1px'.'bem'); // ts(2345)
Copy the code

We can also use type aliases to pull out the above joined types and then join them further,

type ModernUnit = 'vh' | 'vw';
type Unit = 'px' | 'em' | 'rem';
type MessedUp = ModernUnit | Unit; / / type is' the vh '|' vw '|' px '|' em '|' rem '
Copy the code

We can also combine interface types to represent more complex structures.

interface Bird {
  fly(): void;
  layEggs(): void;
}
interface Fish {
  swim(): void;
  layEggs(): void;
}
const getPet: () = > Bird | Fish = () = > {
  return {
   // ...
  } as Bird | Fish;
};
const Pet = getPet();
Pet.layEggs(); // ok
Pet.fly(); // ts(2339) 'Fish' has no 'fly' attribute; 'the Bird | Fish' not 'fly' properties
Copy the code

In a union type, we can directly access properties and methods owned by each interface member without prompting for type errors. However, if there are attributes or methods that are unique to individual members, we need to differentiate between them. In this case, we need to introduce type guards (see section 11) to differentiate between different member types.

However, in this case, we also need to use a type guard based on the in operator

if (typeof Pet.fly === 'function') { // ts(2339)
  Pet.fly(); // ts(2339)
}
if ('fly' in Pet) {
  Pet.fly(); // ok
}
Copy the code

8.2 Crossover Type

Intersection types exist in TypeScript as logic and behavior types. Intersection types combine multiple types into a single Type that has all the properties of member types.

In TypeScript, we can declare cross types using the & operator,

{
  type Useless = string & number;
}
Copy the code

If we simply combine atomic types, such as primitive types, literal types, function types, and so on, into cross types, it is Useless because no type can satisfy the need to belong to more than one atomic type at a time. Therefore, in the above code, the type with the alias of Useless is a never.

8.3 Merging interface Types

The real use of federated types is to merge multiple interface types into a single type, thus achieving the effect of identical interface inheritance. This is called merging interface types

type IntersectionType = { id: number; name: string; } and {age: number };
  const mixed: IntersectionType = {
    id: 1.name: 'name'.age: 18
  }
Copy the code

We make IntersectionType have all the attributes of ID, name and age at the same time by crossing type. Here, we can try to understand merging interface type as seeking union.

Here, let’s think outside the envelope: what happens if multiple interface types that are merged have attributes of the same name?

At this point, we can separate this issue based on whether the types of attributes with the same name are compatible (see section 12).

If the type of the name attribute of the same name is incompatible, as in the example above, the name attribute of the two interface types with the same name is of type number and type string. After merging, the type of the name attribute is the intersection of the atomic types number and string, which is never. The following code looks like this:

type IntersectionTypeConfict = { id: number; name: string; } and {age: number; name: number; };
  const mixedConflict: IntersectionTypeConfict = {
    id: 1.name: 2.// ts(2322) error, type 'number' cannot be assigned to type 'never'
    age: 2
  };
Copy the code

If the types of attributes with the same name are compatible, for example, one is number and the other is a subtype of number and a numeric literal, then the type of the merged name attribute is a subtype of the two.

In the example shown below, the type of the name attribute is the numeric literal type 2, so we cannot assign any value other than 2 to the name attribute.

type IntersectionTypeConfict = { id: number; name: 2; } and {age: number; name: number; };
  let mixedConflict: IntersectionTypeConfict = {
    id: 1.name: 2.// ok
    age: 2
  };
  mixedConflict = {
    id: 1.name: 22.// Type '22' cannot be assigned to type '2'
    age: 2
  };
Copy the code

8.3 Merging the Union type

In addition, we can merge the union types into a crossover type that satisfies different union type constraints, i.e. extracts members of the same type from all the union types. Here, we can also think of merging the union type as finding the intersection.

In the following example, two joint types cross out the type of IntersectionUnion actually equivalent to the ’em’ | ‘rem, so we can only put’ em ‘or’ rem ‘string assigned to IntersectionUnion types of variables.

type UnionA = 'px' | 'em' | 'rem' | The '%';
  type UnionB = 'vh' | 'em' | 'rem' | 'pt';
  type IntersectionUnion = UnionA & UnionB;
  const intersectionA: IntersectionUnion = 'em'; // ok
  const intersectionB: IntersectionUnion = 'rem'; // ok
  const intersectionC: IntersectionUnion = 'px'; // ts(2322)
  const intersectionD: IntersectionUnion = 'pt'; // ts(2322)
Copy the code

If there is no member of the same type in multiple union types, then the type that intersects is never

type UnionC = 'em' | 'rem';
  type UnionD = 'px' | 'pt';
  type IntersectionUnionE = UnionC & UnionD;
  const intersectionE: IntersectionUnionE = 'any' as any; // ts(2322) cannot be given the 'never' type
Copy the code

8.4 Union and cross combination

In the previous example, we pulled out some of the union and crossover types into type aliases, which we further joined and crossed as atomic types. In fact, joint, cross type itself can directly use a combination of, this involves the |, & operator priority issues. In fact, joint, cross operator on the behavior performance not only consistent, is still in operation priority and JavaScript logic or | |, logic and && operator performance.

Joint operator | priority & below the cross operator, also, we can through the use of the small bracket () to adjust the priority of the operators.

type UnionIntersectionA = { id: number; } and {name: string; } | { id: string; } and {name: number; }; // The cross operator takes precedence over the union operator
  type UnionIntersectionB = ('px' | 'em' | 'rem' | The '%') | ('vh' | 'em' | 'rem' | 'pt'); // Adjust the priority
Copy the code

We can also introduce basic rules such as distributive rate and commutative law into type combination, and then optimize for more concise and clear types

 type UnionIntersectionC = ({ id: number; } and {name: string; } | { id: string; & {})name: number; };
  type UnionIntersectionD = { id: number; } and {name: string; } and {name: number; } | { id: string; } and {name: number; }; // Satisfy the distribution rate
  type UnionIntersectionE = ({ id: string; } | { id: number; } and {name: string; & {})name: number; }; // Satisfy the commutative law
Copy the code

8.5 Type Reduction

What happens if you combine a string primitive type and a “string literal type” into a federated type? The effect is that the type is reduced to string. The same reduction logic applies to number, Boolean (and actually enumerations, see section 9)

type URStr = 'string' | string; // The type is string
  type URNum = 2 | number; // The type is number
  type URBoolen = true | boolean; // The type is Boolean
  enum EnumUR {
    ONE,
    TWO
  }
  type URE = EnumUR.ONE | EnumUR; // The type is EnumUR
Copy the code

TypeScript reduces this scenario by reducing literal types and enumerator types to only parent types, such as primitive types and enumerations, which is a reasonable “optimization.”

This reduction, however, significantly reduces the ability of the IDE to automate hints

 type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string; // The type is reduced to string
Copy the code

In the above code, we wanted the IDE to automatically prompt for the string literals for the annotation, but because the type was reduced to string, all the string literals black, red, and so on were not automatically prompted. Don’t panic, the TypeScript official actually provides a dark magic that allows type reduction to be controlled. As shown in the following code, we just need to add “& {}” to the parent type.

  type BorderColor = 'black' | 'red' | 'green' | 'yellow' | 'blue' | string & {}; // Literal types are retained
Copy the code

At this point, other literal types are not reduced, and string literals black, red, and so on are automatically prompted in the IDE.

In addition, when the members of the union type are interface types, if the properties of one of the satisfying interfaces are a subset of the properties of the other interface, this property will also be type reduced, as shown in the following code:

 type UnionInterce =
  | {
      age: '1';
    }
  | ({
      age: '1' | '2';
      [key: string]: string;
    });
Copy the code

Here for ‘1’ is’ 1 ‘|’ 2 ‘subset, so the age of the property into a’ 1 ‘|’ 2 ‘.

Using this property, let’s implement the fogey laid down in lecture 07, how to define the age attribute as a number type, while the other uncertain attribute is a string type data structure object?

{
  age: 1.// Numeric type
  anyProperty: 'str'.// All other undetermined attributes are strings. }Copy the code

The core of the problem is to find a subtype that is both number and age, so that the type reduced by age is number. It is also a subtype of string to satisfy the constraint of the attribute and the string index type.

Which type satisfies this condition? Let’s recall the special type never from lecture 02.

A property of never is that it is a subtype of all types, as well as number and string, so the answer looks like this:

type UnionInterce =
  | {
      age: number;
    }
  | ({
      age: never;
      [key: string]: string;
    });
  const O: UnionInterce = {
    age: 2.string: 'string'
  };
Copy the code

After learning and mastering associative and cross-disciplinary types, we can develop the awareness and ability to extract and reuse common types.

Enumeration types: explain the seven common uses of enumeration types

A type with the advantages of semantics and concise values, used to represent a collection of named integer constants.

9.1 Enumeration Types

There is no concept of matching enumerations in the JavaScript native language, but TypeScript implements enumerated types (Enums), which means that enumerations are typescript-specific (as opposed to JavaScript) syntax.

In TypeScript, you can use enumerations to define collections of named constants. TypeScript supports enumerations for numeric and character constants.

We can also use the enum keyword to define an enumeration type in the format enum + the name of the enumeration + a pair of parentheses with the named constant members in the parentheses.

enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
  }  
Copy the code

Note: The enum is a special type compared to other types because it is both a value and a type, similar to a class (when we define a class structure, we also automatically define the type of the class instance).

In the example above, Day can represent either a collection or a type of the collection, and all members (enum members) are subtypes of Day.

As we said earlier, there is no original implementation of enumerations in JavaScript. The TypeScript translator translates enumerations into objects with constant properties and named values incrementing from zero. Achieve consistency with enumerations at the functional level (not all features are implemented in JavaScript, however).

Translated into JavaScript:

var Day = void 0;
    (function (Day) {
        Day[Day["SUNDAY"] = 0] = "SUNDAY";
        Day[Day["MONDAY"] = 1] = "MONDAY";
        Day[Day["TUESDAY"] = 2] = "TUESDAY";
        Day[Day["WEDNESDAY"] = 3] = "WEDNESDAY";
        Day[Day["THURSDAY"] = 4] = "THURSDAY";
        Day[Day["FRIDAY"] = 5] = "FRIDAY";
        Day[Day["SATURDAY"] = 6] = "SATURDAY";
    })(Day || (Day = {}));
Copy the code

In TypeScript, we can use the “enumeration of names. Gets the members of an enumeration in the constant name format

function work(d: Day) {
    switch (d) {
      case Day.SUNDAY:
      case Day.SATURDAY:
        return 'take a rest';
      case Day.MONDAY:
      case Day.TUESDAY:
      case Day.WEDNESDAY:
      case Day.THURSDAY:
      case Day.FRIDAY:
        return 'work hard'; }}Copy the code

When translated into JavaScript, the switch branch inside the work function is actually equivalent to the following:

.switch (d) {
      case 0:
      case 1:
        return 'take a rest';
      case 2:
      case 3:
      case 4:
      case 5:
      case 6:
        return 'work hard'; }...Copy the code

This means that there is no logical difference between the enum and the value of the argument passed to the work function in JavaScript, which also conforms to TypeScript static type detection rules

 work(Day.SUNDAY); // ok
 work(0); // ok
Copy the code

We can either use the enumeration member day.Sunday as an input to work, or we can use the numeric literal 0 as an input to work.

Let’s take a look at the seven common enumeration types: numeric, string, heterogeneous, constant and computed (value) members, enumerator types and union enumerations, constant enumerations, and external enumerations.

9.2 Enumeration

As you can see from the above example, when we only specify the constant name, we define a collection of numbers that increments from 0 by default, called a numeric enumeration.

If we want the enumeration value to increase from some other value, we can display the initial value of the specified enumeration member in the format “constant name = value”.

enum Day {
    SUNDAY = 1,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY
  }
Copy the code

In fact, we can specify SUNDAY with any type (such as integer, negative, decimal, and so on), any starting number, and members that do not display the specified value will be incrementing by 1. The above example, translated into JavaScript, is an object whose property value increments from 1,

var Day = void 0;
    (function (MyDay) {
        Day[Day["SUNDAY"] = 1] = "SUNDAY";
        Day[Day["MONDAY"] = 2] = "MONDAY"; . Day[Day["SATURDAY"] = 7] = "SATURDAY";
    })(Day || (Day = {}));
Copy the code

Here day.Sunday is given a value of 1 and day.Saturday is given a value of 7.

We can also assign values to members at any location, as shown in the following example:

enum Day {
    SUNDAY,
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY = 5
  } 
Copy the code

Here we specify an initial value of 5 for the last member, SATURDAY, but the result is translated

. Day[Day["FRIDAY"] = 5] = "FRIDAY";
        Day[Day["SATURDAY"] = 5] = "SATURDAY"; .Copy the code

We can see that myday. FRIDAY and myday. SATURDAY both have the number 5. This causes the function work to use the Day enumeration as the switch branch condition. The branch of Myday. FRIDAY is also entered and a logic error occurs.

The rule of thumb is that it is unwise to explicitly specify a value to enumerators of some numeric types or to pass a value to a function rather than an enumeration type as an input, since the default value of enumerations is self-incrementing and completely unstable

enum Day {
    ...
    SATURDAY = 5 // bad
  } 
  work(5); // bad
Copy the code

In addition, two enumerations with the same name and structure order will still have the same value (satisfying the identity ===) even after being translated into JavaScript. But in TypeScript’s view, they are not identical and do not satisfy identity

 enum MyDay {
    SUNDAY,
    ...
  } 
  
  Day.SUNDAY === MyDay.SUNDAY; // ts(2367) Two enumerations are always unequal
  work(MyDay.SUNDAY); // ts(2345) 'myday.sunday' cannot be given 'Day'
Copy the code

If we compare MyDay to a member of Day (line 6), or if we pass MyDay to a work function that takes a parameter of type Day (line 7), we will see an error.

This is an extremely safe design because all enumerations, not just enumerations of numeric types, are self-compatible, which eliminates the risk of enumerations being unstable. However, this can make enumerations less useful, because different enumerations are completely incompatible with each other, and many TypeScript programmers consider enumerations to be unusable types. Two enumerations with exactly the same structure are more likely to be expected if they are compatible, such as multiple enumerations with the same structure describing the same set of constants in different modules automatically generated based on Swagger.

However, at this point we may have to use type assertion (as) or refactor the code to extract the “same” enumeration types into the same common enumeration (the latter is preferred).

9.3 String Enumeration

An enumeration whose value is defined as a string literal is called a string enumeration

enum Day {
    SUNDAY = 'SUNDAY',
    MONDAY = 'MONDAY'. }Copy the code

After the translation,

var Day = void 0;
    (function (Day) {
        Day["SUNDAY"] = "SUNDAY";
        Day["MONDAY"] = "MONDAY";
    })(Day || (Day = {}));
Copy the code

9.4 Heterogeneous Enumeration (Heterogeneous Enums)

TypeScript supports enumerations that have members of both numeric and character types. Such enumerations are called heterogeneous enumerations.

enum Day {
    SUNDAY = 'SUNDAY',
    MONDAY = 2. }Copy the code

The value of an enumeration member can be either a constant such as a number or a string, or it can be a value computed from an expression. This involves a classification of members in an enumeration, namely constant members and computed members.

9.5 Constant members and computed (value) members

In the previous examples, the enumeration members involved were all strings, numeric literals, and numeric constants that increments from 0 with an unspecified initial value, all called constant members.

In addition, during translation, members that define values through the constant enumeration expression that is evaluated are also called constant members, such as in the following cases:

References from a predefined constant member, such as from the current enumeration or another enumeration;

Constant enumeration expressions wrapped in parentheses ();

The unary operators +, -, and ~ applied to constant enumeration expressions;

Constant enumeration operation expression of binary operators +, -, *, /, %, < <, > >, > > >, &, |, ^.

Except in these cases, all are considered members of the calculation.

In the example shown below (quoted from the official example), all are constant members except that G is a computed member.

 enum FileAccess {
    // Constant member
    None,
    Read = 1 << 1,
    Write = 1 << 2,
    ReadWrite = Read | Write,
    // Count members
    G = "123".length,
  }
Copy the code

Note: The division between constant members and computed members is hard to understand, and is not really useful, except to show that there are ways to define values for enumerators. Therefore, it is enough to remember that default values (incrementing from 0), numeric literals, and string literals must be constant members.

9.6 Enumerating Member Types and union enumerations

In addition, members of constant type that do not need to be evaluated, i.e., default values (incrementing from 0), numeric literals, and string literals (which is why we only need to remember these three cases), are called literal enumerators.

Earlier we mentioned that enumeration values and types are integrated, and that the type of an enumeration member is a subtype of the enumeration type.

There are two cases of the relationship between an enumeration member and an enumeration type. If the enumeration member contains both literal and non-literal enumeration values, the type of the enumeration member is the enumeration itself (which is also a subtype of the enumeration itself). If all enumeration members are literal enumeration values, then all enumeration members are both values and types

 enum Day {
    SUNDAY,
    MONDAY,
  }
  enum MyDay {
    SUNDAY,
    MONDAY = Day.MONDAY
  }
  const mondayIsDay: Day.MONDAY = Day.MONDAY; // ok: Literal enumeration members are both values and types
  const mondayIsSunday = MyDay.SUNDAY; // ok: the type is MyDay, myday. SUNDAY is just the value
  const mondayIsMyDay2: MyDay.MONDAY = MyDay.MONDAY; // ts(2535), MyDay contains non-literal value members, so myday. MONDAY cannot be a type
Copy the code

Here, because all members of Day are literal enumeration members, day.monday can be used as both a value and a type (line 11). But MyDay’s member MONDAY is a nonliteral (but constant) enumerator, so myday. MONDAY can only be used as a value

In addition, if the enumeration has only one member and it is a literal member, then the type of that member is equal to the enum type, as shown in this code:

enum Day {
  MONDAY
}
export const mondayIsDay: Day = Day.MONDAY; // ok
export const mondayIsDay1: Day.MONDAY = mondayIsDay as Day; // ok
Copy the code

Because the enumeration Day contains only a literal member MONDAY, the types Day and day.monday are compatible. For example, on lines 4 and 5, we can assign the type Day.MONDAY to the type Day and Day to the type day.monday.

In addition, recall from lecture 04 that the properties of literals are that different member types are different literal types. A pure literal member enumeration type also has the properties of a literal type, which means that the type equivalent to an enumeration becomes a union (enumeration) type of the individual member types.

Union types allow TypeScript to more clearly enumerate the exact values in a set, thus detecting conditions (known as Dead codes) that would never be true.

 enum Day {
    SUNDAY,
    MONDAY,
  }
  
  const work = (x: Day) = > {
    if(x ! == Day.SUNDAY || x ! == Day.MONDAY) {// ts(2367)}}Copy the code

In the above example, TypeScript determines that the value of x is either day.Sunday or day.monday. Because Day is pure literal enumerated types, which can be equivalent to as joint type day.to SUNDAY | Day. MONDAY, so we judge the line 7 conditional statement is true, then prompted an error ts (2367).

However, this is not the case if the enumeration contains members that need to be evaluated. In the following example, TypeScript cannot enumerate each member of Day separately. Because each member is of type Day, the conditional statement on line 7 cannot be determined to be true and a TS (2367) error is not presented.

 enum Day {
    SUNDAY = +'1',
    MONDAY = 'aa'.length,
  }
  
  const work = (x: Day) = > {
    if(x ! == Day.SUNDAY || x ! == Day.MONDAY) {// ok}}Copy the code

The same type inference and type reduction features that literal types have also apply to literal enumerated types, as shown in the following code

enum Day {
    SUNDAY,
    MONDAY,
  }
  let SUNDAY = Day.SUNDAY; // The type is Day
  const SUNDAY2 = Day.SUNDAY; / / type Day. SUNDAY
  const work = (x: Day) = > {
    if (x === Day.SUNDAY) {
      x; // The type is narrowed to day.sunday}}Copy the code

In the above code, we define an unexplicitly typed SUNDAY variable in line 5 with a let. TypeScript can infer that its type is Day; On line 6, we define a variable of undefined type SUNDAY2 as const. TypeScript deduces that its type is day.sunday. In the if condition on line 8, the variable x type is also reduced from Day to day.sunday.

9.7 Constant enumeration (const enums)

We can define a constant enumeration by adding the const modifier. A constant enumeration definition is removed when translated into JavaScript and is replaced with the corresponding inline value where it is used. Therefore, all members of a constant enumeration must be constant members (literals + expressions that evaluate values during translation).

const enum Day {
    SUNDAY,
    MONDAY
  }
  const work = (d: Day) = > {
    switch (d) {
      case Day.SUNDAY:
        return 'take a rest';
      case Day.MONDAY:
        return 'work hard'; }}}Copy the code

When translated to JavaScript, the definition of the Day enumeration is removed and the reference to Day in the work function is changed to a constant value (line 3 is inlined with 0, line 5 is inlined with 1).

var work = function (d) {
        switch (d) {
            case 0 /* SUNDAY */:
                return 'take a rest';
            case 1 /* MONDAY */:
                return 'work hard'; }};Copy the code

To sum up, using constant enumerations not only reduces the amount of translated JavaScript code (because the enumeration definition is erased), but also eliminates the need to look up the enumeration definition in the upper scope (because the enumeration literal is directly inlining).

Therefore, by defining constant enumerations, we can maintain associated collections of constants, such as Switch Case branches, in a clear and structured manner, making the code more readable and maintainable. And because the definition and inline member values are erased after translation, there is no difference in code volume and performance over directly inline constant values.

9.8 External Enumeration (Ambient Enums)

In TypeScript, we use declare to describe a variable that is already defined elsewhere

declare let $: any;
$('#id').addClass('show'); // ok
Copy the code

In line 1 we use Declare to describe an external variable of type any, immediately in line 2, immediately in line 2, and immediately in line 2, without prompting an error that the $variable was not found.

Similarly, we can use Declare to describe an enumeration that has already been defined elsewhere. An enumeration defined in this way is called an external enumeration

declare enum Day {
  SUNDAY,
  MONDAY,
}
const work = (x: Day) = > {
  if (x === Day.SUNDAY) {
    x; // The type is Day}}Copy the code

Here we assume that an enumeration of the structure Day is already defined elsewhere and that it is used in the work function.

When translated into JavaScript, the definition of an external enumeration is erased just like a constant enumeration, but the reference to the enumeration member is retained (line 2 keeps the reference to day.Sunday).

var work = function (x) {
    if(x === Day.SUNDAY) { x; }};Copy the code

External enumerations differ from regular enumerations in the following ways:

In an external enumeration, any member that does not have an initial value is treated as a computed (value) member, as opposed to a regular enumeration;

Even if the external enumeration contains only literal members, the types of those members would not be literal members, and would naturally have none of the properties of literal types.

We can use declare and const together to define an external constant enumeration that still looks like a constant enumeration when translated into JavaScript. While erasing the enumeration definition, we can replace references to enumeration members with inline enumeration values.

The purpose of an external enumeration is to provide a way for members of two different enumerations (which actually refer to the same enumeration type) to be compatible, compared, and reused, which improves the availability of enumerations to some extent

Core knowledge points and suggestions:

  • Using constant enumerations to manage related constants can improve the readability and maintainability of code.

  • Do not replace the enumeration members used with any other type;

  • External enumerations typically appear in type declaration files (.d.ts) to describe enumerated types defined elsewhere. For example, in types.d.ts you can declare enum A {… } Describe the actual enumeration defined in business.ts enum A {… }; This makes enum A compatible between business.ts and enum A in types.d.ts. External enumeration

10 the generic

One of typescript’s most interesting types is that we can use generics to constrain type variables

Two questions:

  • One is how to use TypeScript to implement functions similar to call (or apply) functionality, with emphasis on candidates’ use of generics.
  • The other one is what is a generic? What do generics do? Focus on the candidate’s understanding of generics.

Generics are a very basic, essential (and challenging) feature of TypeScript, bridging and sublimating the gap between TypeScript’s beginnings (which focus on the basics) and its more advanced (which focus on application practices).

10.1 What are Generics?

Generics refers to type parameterization, i.e. the parameterization of a specific type. Just as we define function arguments, we can define several type arguments to a generic type and pass explicit type arguments to the generic type on the call. Generics are designed to effectively constrain relationships between members of a type, such as between function parameters and return values, or between class or interface members and methods.

10.2 Generic type parameters

The most common scenario for generics is to constrain the type of a function’s arguments. We can define several types of arguments that are passed to the function when it is called.

Consider the reflect function defined below, which takes an argument of any type and returns the value and type of the argument unchanged. How do we describe this function? I think I’m going to use unknown (actually I was going to say any, because any is the devil, so I’ll use unknown).

function reflect(param: unknown) {
  return param;
}
const str = reflect('string'); // The type of STR is unknown
const num = reflect(1); // Num Type unknown
Copy the code

At this point, the reflect function can take an argument of any type and return the value as it is, but the return value type is not what we expect. This is because we want the return value type to correspond to the input type (such as number to number, string to string), rather than the return value being unknown regardless of the input type.

How do you define a generic parameter? First, we define the type of the parameter param as a (type-level) parameter, variable, not an explicit type, and wait until the explicit type is passed in to the function call.

For example, we can define a generic parameter P to the function using Angle brackets <> syntax, and specify the type of the param argument to be P, as follows:

function reflect<P> (param: P) {
  return param;
}
Copy the code

The P in Angle brackets indicates the definition of the generic parameter, and the P after param indicates that the type of the parameter is the generic P (that is, the type is p-constrained).

We can also use generics to explicitly annotate the type of the return value, although this is not necessary (since the type of the return value can be inferred based on the context). For example, when reflecting as shown below, we can explicitly pass an explicit type to the generic parameter P using Angle bracket <> syntax.

function reflect<P> (param: P) :P {
  return param;
}
Copy the code

Then, when we call the function, we also specify the types string and number as shown in <> syntax. Accordingly, reflectStr is of type string and reflectNum is of type number.


const reflectStr = reflect<string>('string'); // The type of STR is string
const reflectNum = reflect<number>(1); // Num Type number
Copy the code

In addition, if a generic function is called with a generically constrained parameter that has a pass-value, the input arguments to the generic parameter can be inferred from the type of the parameter without specifying the type explicitly (by default), so the above example can be abbreviated as the following:

const reflectStr2 = reflect('string'); // The type of STR is string
const reflectNum2 = reflect(1); // Num Type number
Copy the code

Generics can restrict not only the types of the entire parameters of a function, but also the types of the parameters’ attributes and members. For example, the parameters’ types can be arrays or objects

function reflectArray<P> (param: P[]) {
  return param;
}
const reflectArr = reflectArray([1.'1']); // reflectArr 是 (string | number)[]
Copy the code

Generics allow us to constrain the type relationship between function parameters and return values. For example, in the React Hooks useState scenario, the first element in the return tuple on line 2 (because useState returns an array of length 2 with a fixed element type) is of the generic type S. The parameter type of the second function type element is also the generic S.

function useState<S> (state: S, initialValue? : S) {
  return [state, (s: S) = > void 0] as unknown as [S, (s: S) = > void];
}
Copy the code

Note: A function’s generic input must have valid constraints on its parameters/parameter members to be meaningful. For example, in the following example, we define a generic type that constrains only the type of the return value, which makes no sense.

function uselessGenerics<P> () :P {
  return void 0 as unknown as P;
}
Copy the code

We can define any number of generic parameters to a function

function reflectExtraParams<P.Q> (p1: P, p2: Q) :P.Q] {
  return [p1, p2];
}
Copy the code

We define a function reflectExtraParams with two generic input parameters (P and Q) and constrain the type of function parameters P1 and P2 and the return value by P and Q.

10.3 a generic class

In class definitions, we can also use generics to constrain the types of constructors, properties, and methods

class Memory<S> {
  store: S;
  constructor(store: S) {
    this.store = store;
  }
  set(store: S) {
    this.store = store;
  }
  get() {
    return this.store; }}const numMemory = new Memory<number>(1); // 
      
        can default
      
const getNumMemory = numMemory.get(); // The type is number
numMemory.set(2); // Only the number type can be written
const strMemory = new Memory(' '); The default < / string >
const getStrMemory = strMemory.get(); // The type is string
strMemory.set('string'); // Only the string type can be written
Copy the code

Generic classes are similar to generic functions in that when a class instance is created, if the generic-constrained parameters are passed explicit values, then the generic input parameters (specifically, the types passed) can default, such as the

,

generic input parameters on lines 14 and 18.

For React developers, components also support generics

function GenericCom<P> (props: { prop1: string }) {
  return <></>;
};
<GenericCom<{ name: string; }> prop1="1". />Copy the code

On lines 1 through 3, we define a generic component GenericCom that takes a type input parameter P. In line 4, we also explicitly specify the interface type {name: string} as the input parameter when we create the component element using the JSX syntax.

10.4 Generic types

In TypeScript, a type can itself be defined as a generic type that has undefined type parameters, and can take explicit types as input arguments, thereby spawning more specific types

const reflectFn: <P>(param: P) = > P = reflect; // ok
Copy the code

Here we explicitly annotate the generic type variable reflectFn and assign the reflect function to it as a value.

We can also extract the reflectFn’s type annotation as a type alias or interface that can be reused

type ReflectFuncton = <P>(param: P) = > P;
interface IReflectFuncton {
  <P>(param: P): P
}
const reflectFn2: ReflectFuncton = reflect;
const reflectFn3: IReflectFuncton = reflect;
Copy the code

A generic type is defined when the definition of a type input parameter is moved to a type alias or interface name. A type that receives a specific type input parameter and then returns a new type is defined as a generic type.

type GenericReflectFunction<P> = (param: P) = > P;
interface IGenericReflectFunction<P> {
  (param: P): P;
}
const reflectFn4: GenericReflectFunction<string> = reflect; // Reify generics
const reflectFn5: IGenericReflectFunction<number> = reflect; // Reify generics
const reflectFn3Return = reflectFn4('string'); // Both the input and return values must be string
const reflectFn4Return = reflectFn5(1); // Both the input and return values must be of type number
Copy the code

In the definition of a generic type, we can even use type operators to express the operation so that the types of the generic type can be derived from the types of the input parameters

type StringOrNumberArray<E> = E extends string | number ? E[] : E;
type StringArray = StringOrNumberArray<string>; // The type is string[]
type NumberArray = StringOrNumberArray<number>; // The type is number[]
type NeverGot = StringOrNumberArray<boolean>; // The type is Boolean
Copy the code

Here we define a generic, if the reference is number | string will generate an array type, otherwise we are generated into the parameter type. In addition, we use the same syntax as JavaScript ternary expressions to express the logical relationships of type operations, which will be covered in more detail in Lecture 15.

Divergence, if we give the above introduced into a generic string | as a Boolean combination type into, will be what kind?

type BooleanOrString = string | boolean;
type WhatIsThis = StringOrNumberArray<BooleanOrString>; / / like that should be a string | Boolean?
type BooleanOrStringGot = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString; // string | boolean
Copy the code

Alias WhatIsThis hover types, then you will find that the type of display will be Boolean | string [].

BooleanOrStringGot and WhatIsThis have different types of aliases. What logic is this? This is known as Distributive Conditional Types.

The official definition of the concept of assigning conditional types is that in the case of conditional types (such as extends in the previous example), if the input is of a union type, it is broken up into individual (atomic) types (members) for type operation.

Such as in the example above string | Boolean arguments, be dissected into string and Boolean this two independent type, respectively to determine whether the string | a subset of number type. Because the string is subset and Boolean not, so eventually we got WhatIsThis type is Boolean | string [].

Generic types that take in parameters, like functions, can evaluate the input type and return the new type, as if they were performing a type operation.

With generics, we can abstract and encapsulate many useful and complex type constraints. For example, to constrain the type definition relationship of State and Reducers in the Redux Model, we can define an interface type generics that can not only accept the input of State type, but also contain the two attributes of State and Reducers through the following code. The type relationship between the generic State attribute and the action index attribute under the reducers attribute is constrained by the State input parameter.

interface ReduxModel<State> {
  state: State,
  reducers: {
    [action: string]: (state: State, action: any) = > State
  }
}
Copy the code

We then pass in a concrete State type to instantiate the ReduxModel and constrain an actual model as needed

type ModelInterface = { id: number; name: string };
const model: ReduxModel<ModelInterface> = {
  state: { id: 1.name: 'xiaoming' }, // Ok type must be ModelInterface
  reducers: {
    setId: (state, action: { payload: number }) = > ({
      ...state,
      id: action.payload // ok must be number
    }),
    setName: (state, action: { payload: string }) = > ({
      ...state,
      name: action.payload // ok must be string}}})Copy the code

The type of the state attribute of the Model object, the setId of the reducers attribute, and the first parameter state of the setName method are all constrained by the ReduxModel generic input parameter ModelInterface.

Note: Enumeration types do not support generics.

10.5 Generic constraints

As mentioned earlier, generics are functions of types that can abstract, encapsulate, and receive incoming parameters, and the incoming parameters of generics have properties similar to the incoming parameters of functions. Therefore, we can restrict the generic input parameters to a relatively unambiguous set.

For example, in the previous example of reflect, which returns an argument intact, we want to limit the type of the received argument to a collection of primitive types. We can use the “generic input name extends type” syntax to do this

function reflectSpecified<P extends number | string | boolean> (param: P) :P {
  return param;
}
reflectSpecified('string'); // ok
reflectSpecified(1); // ok
reflectSpecified(true); // ok
reflectSpecified(null); / / ts (2345) 'null' not give type 'number | string | Boolean'
Copy the code

We limit the generics in ginseng to is number | string | Boolean subset.

Similarly, we can restrict the interface generic input parameters to a specific scope

interface ReduxModelSpecified<State extends { id: number; name: string }> {

  state: State

}

type ComputedReduxModel1 = ReduxModelSpecified<{ id: number; name: string; } >.// ok

type ComputedReduxModel2 = ReduxModelSpecified<{ id: number; name: string; age: number; } >.// ok

type ComputedReduxModel3 = ReduxModelSpecified<{ id: string; name: number; } >.// ts(2344)

type ComputedReduxModel4 = ReduxModelSpecified<{ id: number; } >.// ts(2344)

Copy the code

In the example above, the ReduxModelSpecified generic only receives {id: number; Name: string} The subtype of the interface type is used as an incoming parameter.

We can also set constraints between multiple different generic input parameters

 interface ObjSetter {
     <O extends {}, K extends keyof O.V extends O[K] >(obj: O, key: K, value: V): V;
 }
 const setValueOfObj: ObjSetter = (obj, key, value) => (obj[key] = value);
 setValueOfObj({ id: 1, name: 'name' }, 'id', 2); // ok
 setValueOfObj({ id: 1, name: 'name' }, 'name', 'new name'); // ok
 setValueOfObj({ id: 1, name: 'name' }, 'age', 2); // ts(2345)
 setValueOfObj({ id: 1, name: 'name' }, 'id', '2'); // ts(2345)
Copy the code

When the function type that sets the value of an object property is set, it takes three generic parameters: The first is an object, the second is a subset of the first set of input property names, and the third is a subtype that specifies the type of the property (this uses the keyof operator; we’ll cover TypeScript type-related operators in more detail in Lecture 15).

In addition, a generic input parameter is similar in that it can specify default values (default types) for the generic input parameter, and the syntax is the same as that for the specified function default parameter

interface ReduxModelSpecified2<State = { id: number; name: string }> {
    state: State
}
type ComputedReduxModel5 = ReduxModelSpecified2; // ok
type ComputedReduxModel6 = ReduxModelSpecified2<{ id: number; name: string; } >.//ok
type ComputedReduxModel7 = ReduxModelSpecified; // ts(2314) is missing a type parameter
Copy the code

We defined the generic type ReduxModelSpecified2 with default input types, so with ReduxModelSpecified2 type input parameters can be default. ReduxModelSpecified has no default value, so a type error will be displayed for default input.

The constraints and default values of generic input parameters can also be combined

interface ReduxModelMixed<State extends {} = { id: number; name: string }> {
    state: State
}
Copy the code

Here we specify that the generic ReduxModelMixed input parameter State must be a subtype of the {} type. We also specify that the default input parameter type is the interface type {id: number; name: string; }.

10.6 summary

We can try to understand generics as functions within types, and achieve type reuse by abstracting and encapsulating type operation logic to better understand generics.

Question to consider: How do I implement Call in TypeScript?

Tip: Parameters, ReturnType. These keywords, like Array, are TypeScript encapsulated generics (the official built-in generics are covered in Lecture 14). You can find out more about TypeScript + keywords in advance by searching “TypeScript + keywords” on Google, Baidu, or the official documentation.