“This is the 15th day of my participation in the August Gwen Challenge.


In JavaScript, we don’t seem to hear much about interfaces, a common feature in TypeScript that gives iT the ability to describe more complex data structures that JavaScript lacks. Let’s look at what an interface type is.

I. Interface definition

An interface is a set of abstract method declarations, a collection of method characteristics that should be abstract and implemented by a concrete class that can then be invoked by a third party to make the concrete class execute the concrete method.

One of TypeScript’s core principles is to type check the structure of a value, and as long as two objects have the same structure and the same type of properties and methods, they are the same type. In TypeScript, interfaces name these types and define contracts for code or third-party code.

TypeScript interfaces are defined as follows:

interface interface_name { }
Copy the code

For example, the function takes an object containing two fields, firstName and lastName, and returns a full concatenated name:

const getFullName = ({ firstName, lastName }) = > {
  return `${firstName} ${lastName}`;
};
Copy the code

Call with passing in arguments:

getFullName({
  firstName: "Hello".lastName: "TypeScript"
}); 
Copy the code

This call is fine, but some errors occur if the argument is passed in a format other than the one you want:

getFullName(); // Uncaught TypeError: Cannot destructure property `a` of 'undefined' or 'null'.
getFullName({ age: 18.phone: 110 }); // 'undefined undefined'
getFullName({ firstName: "Hello" }); // 'Hello undefined'
Copy the code

These are undesirable parameters that are passed in at development time, so TypeScript can detect these errors at compile time. To improve the definition of this function:

const getFullName = ({
  firstName,
  lastName,
}: {
  firstName: string; //The field specifying the attribute names firstName and lastName must have an attribute value ofstringType the lastName:string;
}) = > {
  return `${firstName} ${lastName}`;
};
Copy the code

The structure of the object passed in is defined by its literal form.

getFullName(); // There should be 1 argument, but get 0
getFullName({ age: 18.phone: 110 }); // Type "{age: number; phone: number; } cannot be assigned to type {firstName: string; lastName: string; } "argument.
getFullName({ firstName: "Hello" }); // The required attribute lastName is missing
Copy the code

These are errors that TypeScript prompts when writing code to avoid passing in incorrect arguments when using functions. We can use interface to define an interface:

interface Info {
  firstName: string;
  lastName: string;
}
const getFullName = ({ firstName, lastName }: Info) = >
  return `${firstName} ${lastName}`;
};
Copy the code

Note: When defining an interface, do not think of it as defining an object. The {} parentheses enclose a block of code that contains declarations, except that instead of declaring the value of a variable, it declares the type. The declaration also does not assign an equal sign, but a colon specifies the type. Separate each declaration with a line break, a semicolon, or a comma.

2. Interface attributes

1. Optional attributes

TypeScript provides optional attributes for structures that require optional fields to be processed if they exist and ignored if they don’t.

Define a function:

const getVegetables = ({ color, type }) = > {
  return `A ${color ? color + "" : ""}The ${type}`;
};
Copy the code

Color is optional, so you can set an optional attribute to the interface. You can add a? You can:

interfaceVegetables { color? :string;
  type: string;
}

const getVegetables = ({ color, type }: Vegetables) = > {
  return `A ${color ? color + "" : ""}The ${type}`;
};
Copy the code

Json rules can be turned off by adding “interface-name”: [true, “never-prefix”] to tslint.json rules.

When an attribute is annotated as optional, its type becomes a combination of the explicitly specified type and undefined type, such as the color attribute type in the argument to the getVegetables method:

string | undefined;
Copy the code

Is the interface below the same as the interface above?

interfaceVegetables2 { color? :string | undefined;
  type: string;
}
Copy the code

The answer is definitely no, because optional means you don’t have to set the property key name, and undefined means the property key name is not optional.

2. Read-only attribute

The interface can set read-only properties as follows:

interface Role {
  readonly 0: string;
  readonly 1: string;
}
Copy the code

Here we define a role with both 0 and 1 role ids. Define a role data and modify its value:

const role: Role = {
  0: "super_admin".1: "admin"
};
role[1] = "super_admin"; // Cannot assign to '0' because it is a read-only property
Copy the code

The TypeScript hint here cannot be assigned to index 0 because it is a read-only property.

In ES6, a constant defined with const cannot be changed after it is defined. This is close to read-only. How do you choose between readonly and const? If the value is a constant, use const; if the value is an object property, use readonly:

const NAME: string = "TypeScript";
NAME = "Haha"; // Uncaught TypeError: Assignment to constant variable
const obj = {
  name: "TypeScript"
};
obj.name = "Haha";

interface Info {
  readonly name: string;
}
const info: Info = {
  name: "TypeScript"
};
info["name"] = "Haha"; // Cannot assign to 'name' because it is a read-only property
Copy the code

The above const NAME constant will raise an error. However, if we define an object using const names and then change the values of the attributes in the object, we will not raise an error. Therefore, to ensure that the value of an object’s attribute cannot be modified, use readonly.

Note that readOnly is read-only only at the static type detection level and does not actually prevent changes to the object. Because the readonly modifier is erased after translation to JavaScript. Therefore, it is safer to return a new object any time rather than modify it directly.

3. Redundant attribute check

Let’s start with the following example:

interfaceVegetables { color? :string;
  type: string;
}

const getVegetables = ({ color, type }: Vegetables) = > {
  return `A ${color ? color + "" : ""}The ${type}`;
};

getVegetables({
  type: "tomato".size: "big"     // 'size' is not in type 'Vegetables'
});
Copy the code

The color attribute is not passed in because it is an optional attribute, so no error is reported. However, an error is reported when an additional size attribute is passed in, and TypeScript tells you that the interface does not have this extra attribute, so an error is reported when the interface does not define this attribute.

Json rules to disable this rule, add “object-literal-sort-keys”: [false] to tslint.json rules.

Sometimes we don’t want TypeScript to be so strict about checking data. For example, we just need to ensure that the object passed to getVegetables has a Type attribute. We don’t care if the object is passed to getVegetables with extra attributes, or what type the extra attributes are. You need to bypass the redundant attribute check in the following ways:

(1) Use type assertions

A type assertion tells TypeScript that it has already checked itself to make sure that the type is ok and expects TypeScript not to check for it. This is the simplest way to implement it. Type assertions are defined using the AS keyword (I won’t go into detail here, but I’ll cover type assertions in a later article) :

interfaceVegetables { color? :string;
  type: string;
}

const getVegetables = ({ color, type }: Vegetables) = > {
  return `A ${color ? color + "" : ""}The ${type}`;
};

getVegetables({
  type: "tomato".size: 12.price: 1.2
} as Vegetables);
Copy the code

(2) Add index signatures

A better way is to add a string index signature:

interface Vegetables {
  color: string;
  type: string;
  [prop: string] :any;
}

const getVegetables = ({ color, type }: Vegetables) = > {
  return `A ${color ? color + "" : ""}The ${type}`;
};

getVegetables({
  color: "red".type: "tomato".size: 12.price: 1.2
});
Copy the code

Three, interface use

1. Define function types

In the previous function types article, we said that we can use interfaces to define function types:

interface AddFunc {
  (num1: number.num2: number) :number;
}
Copy the code

The AddFunc structure defines an AddFunc structure, which requires that the value that implements the structure must contain a method with the same parameters and return the same value as the function defined in the structure, or that the value is a function that meets the requirements of the function. The contents wrapped in curly braces are called the call signature, which consists of a list of arguments with argument types and return value types:

const add: AddFunc = (n1, n2) = > n1 + n2;
const join: AddFunc = (n1, n2) = > `${n1} ${n2}`; // Cannot assign type 'string' to type 'number'
add("a".2); // Arguments of type 'string' cannot be assigned to arguments of type 'number'
Copy the code

The add function defined above takes arguments of two numeric types and returns the result as a numeric type, so there is no problem. The join function has the correct parameter type, but returns a string, so an error is reported. When the add function is called, an error is also reported if the parameters passed do not match the type defined by the interface. When you actually define a function, the name does not need to be the same as the parameter name in the interface.

In practice, interface types are rarely used to define function types, but rather inline types or type aliases with arrow function syntax:

type AddFunc = (num1: number, num2: number) = > number;
Copy the code

The arrow function type is given an alias, AddFunc, so you can reuse AddFunc elsewhere without redeclaring the new arrow function type definition.

2. Define the index type

In practice, interface types are commonly used in objects, such as Props & State of the React component. These objects have a common feature, that is, all attribute names and method names are determined.

In fact, objects are often used as maps, such as role1, which has an index of any number, and Role2, which has an index of any string.

const role1 = {
  0: "super_admin".1: "admin"
};
const role2 = {
  s: "super_admin".a: "admin"
};
Copy the code

You need to use index signatures to define the object mapping structure mentioned above and constrain the type of the index by the format “[index name: type]”. Index names can be divided into two types: string and number. Through the RoleDic and RoleDic1 interfaces defined as follows, index names can be used to describe objects whose indexes are arbitrary numbers or strings:

interface RoleDic {
  [id: number] :string;
}

interface RoleDic1 {
  [id: string] :string;
}

const role1: RoleDic = {
  0: "super_admin".1: "admin"
};

const role2: RoleDic = {
  s: "super_admin"."{s: string; a: string; }" assigned to type "RoleDic".
  a: "admin"
};

const role3: RoleDic = ["super_admin"."admin"];
Copy the code

Note that when a number is used as an object index, its type can be compatible with both numbers and strings, which is consistent with JavaScript behavior. Therefore, when you index an object with 0 or ‘0’, the two are equivalent.

Role3 above defines an array with numeric indexes and string values. We can also set the index to readonly to prevent the index return value from being modified:

interface RoleDic {
  readonly [id: number] :string;
}

const role: RoleDic = {
  0: "super_admin"
};

role[0] = "admin"; // Index signatures in error type "RoleDic" can only be read
Copy the code

Note that you can set the index type to number. However, if the property name is set to a string, an error is reported; However, if the index type is set to string, it is fine even if the attribute name is set to a numeric type. If the attribute name is of a numeric type, JS will convert the numeric type to a string and then access it:

const obj = {
  123: "a"."123": "b" // Error: identifier "123" is repeated.
};
console.log(obj); // { '123': 'b' }
Copy the code

If the attribute name of a numeric type is not converted to a string, then the value 123 and string 123 are two different values, then the object obj should have both attributes; But the actual printed obj has only one property, called string “123” with a value of “B”, indicating that the numeric type property name 123 was overwritten because it was converted to string property name “123”; And because multiple attributes in an object with the same attribute name are defined later in the object, the result is that obj only preserves the values of the attributes defined later.

Advanced usage

1. Inherit interfaces

In TypeScript, interfaces are inheritable, just like classes in ES6, which increases interface reusability. Consider a scenario where you define a Vegetables interface that limits the color attribute. Define two interfaces: Tomato and Carrot. These two classes need to limit color, and each class has its own property limits.

interface Vegetables {
  color: string;
}

interface Tomato {
  color: string;
  radius: number;
}

interface Carrot {
  color: string;
  length: number;
}
Copy the code

All three interfaces have a definition of color, but it is tedious to write, can be used to rewrite the inheritance:

interface Vegetables {
  color: string;
}

interface Tomato extends Vegetables {
  radius: number;
}

interface Carrot extends Vegetables {
  length: number;
}

const tomato: Tomato = {
  radius: 1.2 // error  Property 'color' is missing in type '{ radius: number; }'
};

const carrot: Carrot = {
  color: "orange".length: 20
};
Copy the code

The tomato variable defined above reported an error because it lacked the color attribute inherited from the Vegetables interface.

An interface can be inherited by multiple interfaces, and an interface can inherit multiple interfaces separated by commas. For example, if you define a Food interface, Tomato can inherit from Food:

interface Vegetables {
  color: string;
}

interface Food {
  type: string;
}

interface Tomato extends Food, Vegetables {
  radius: number;
}

const tomato: Tomato = {
  type: "vegetables".color: "red".radius: 1
};  
Copy the code

If you want to override inherited attributes, you can only override them with compatible types:

interface Tomato extends Vegetables {
  color: number;
}
Copy the code

Here we overwrite the color attribute and set it to type number. We get an error because the name attribute in Tomato and Vegetables is incompatible.

2. Mixed-type interfaces

In JavaScript, functions are object types. Objects can have properties, so sometimes an object is both a function and contains properties. For example, to implement a counter function, it is more straightforward to define a function and a global variable:

let count = 0;
const counter = () = > count++;
Copy the code

This method, however, requires that a variable be defined outside the function. A better approach is to use closures:

const counter = (() = > {
  let count = 0;
  return () = > {
    returncount++; }; }) ();console.log(counter()); / / 1
console.log(counter()); / / 2
Copy the code

TypeScript supports adding attributes directly to functions, which is supported in JavaScript:

let counter = () = > {
  return counter.count++;
};
counter.count = 0;
console.log(counter()); / / 1
console.log(counter()); / / 2
Copy the code

Here we assign a function to countUp and bind it to the count property, where the count is stored.

We can use the mixed type interface to specify the type of counter in the above example:

interface Counter {
  (): void; 
  count: number; 
}

const getCounter = (): Counter= > { 
  const c = () = > { 
    c.count++;
  };
  c.count = 0; 
  return c; 
};
const counter: Counter = getCounter(); 
counter();
console.log(counter.count); / / 1
counter();
console.log(counter.count); / / 2
Copy the code

Here we define a Counter interface. This structure must contain a function that takes no arguments and returns void, i.e., no value. The structure must also contain an attribute named count with a value of type number. Finally, the counter is obtained using the getCounter function. Here getCounter is of type Counter, it’s a function that has no return value, that is, a return value of type void, and it has a property count that has a return value of type number.

Type aliases

A type alias is not the content of an interface type, but it is similar to the interface functionality, so it is put together here.

1. Basic use

The interface type is used to separate the inline type from the inline type to enable type reuse. In fact, you can reuse the extracted inline types using type aliases. You can define a type alias, such as the type of the counter method defined above, using the format “type alias name = type definition” as follows:

type Counter = {
  (): void; 
  count: number; 
}
Copy the code

This looks like defining variables in JavaScript, except that var, let, and const are replaced with type. In fact, type aliases can be used in scenarios where interface types cannot be overridden, such as union types, cross types, and so on:

// Union type
type Name = number | string;

// Cross types
type Vegetables = {color: string.radius: number} and {color: string.length: number}
Copy the code

A Vegetables type alias is defined to represent the type that two anonymous interface types intersect.

Note that a type alias simply aliases a type, not creates a new type.

2. Different from interfaces

From the above, you can see that in most cases you can use type aliases instead. Does this mean that the two are equivalent? The answer must be no, or these two concepts would not have been developed. In certain scenarios, the two are quite different. For example, if an interface type is defined repeatedly, its attributes are superimposed. This feature makes it easy to extend global variables and third-party library types:

interface Vegetables {
  color: string;
}

interface Vegetables {
  radius: number;
}

interface Vegetables {
  length: number;
}

let vegetables: Vegetables = {
	color:  "red".radius: 2.length: 10
}
Copy the code

Here we define the Vegetables interface three times, which can be assigned to an object containing color, RADIUS, and length without error.

If type aliases are defined repeatedly:

type Vegetables = {
  color: string;
}

type Vegetables = {
  radius: number;
}

type Vegetables = {
  length: number;
}

let vegetables: Vegetables = {
	color:  "red".radius: 2.length: 10
}
Copy the code

The above code returns an error: ‘Vegetables’ is already defined. Therefore, interface types are repeatable and attributes are superimposed, whereas type aliases are non-repeatable.