One of the core concepts of TS is that type checking focuses primarily on the structure of a value. This is sometimes called ‘duck typing’ and ‘structural subtyping’. In TS, interfaces are primarily used to name the types of these values, and are a powerful way to define variable types both inside and outside a project.
First interface
Let’s create a simple interface for understanding:
function printLabel(lableObject: { label: string }) {
console.log(labelObject.label);
}
let myObj = {
size: 10,
label: 'Size 10 Object',
}
printLabel(myObj);
Copy the code
Type checking checks printLable. This method takes only one argument, which is an object with a string property named label. Note that our call object actually has more properties than the required parameter object, but type checking only cares that the call object has at least this property and that the value is of a type that meets the criteria. There are cases where TS is less lenient, but we’ll talk about that later.
Use the above example again:
interface LabeledValue {
label: string
}
function printLabel(labelObj: LabeledValue) {
console.log(lableObj.label)
}
let obj = {size: 10, lael: '10'}
printLabel(obj)
Copy the code
The LabelValue interface is used to describe the requirements for the parameters in our first example. Notice that we don’t explicitly declare that objects need to inherit interfaces. We’re just going to focus on structure in THIS TS. A function is allowed to be called only if the object being called meets the requirements.
Note that type detection does not care about the order in which the types of objects are called, but only whether the attribute exists and whether its value meets the condition.
Optional attribute
Not all attributes in an interface are necessary. These optional attributes are useful when we build something like ‘option packs’.
Like this:
interface SquareConfig{ color? : string, width? : number, } function createSquare(config: SquareConfig): {color: string, area: number} { let newSquare = { color: 'white', area: 100 }; if (config.color) { newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare; } let mySquare = createSquare({ color: "black" });Copy the code
Like other interfaces, optional attributes are defined with a ‘? ‘.
The advantage of optional properties is that you can describe those properties that might be available, while also preventing the use of properties that are not part of the interface. For example, if we typed the wrong variable name in the previous example, we would get an error message at compile time:
if (config.clor) { // Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'? Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'? // Error: Property 'clor' does not exist on type 'SquareConfig' newSquare.color = config.clor; // Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'? Property 'clor' does not exist on type 'SquareConfig'. Did you mean 'color'? }Copy the code
Read-only property
Some properties should only be modified at creation time. You can specify these properties by adding a readonly field.
interface Point{
readonly x:number;
readonly y:number;
}
Copy the code
You can build a Point object by assigning literal objects directly. After the declaration, x and y are immutable.
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
//Cannot assign to 'x' because it is a read-only property.
Copy the code
TS comes with a ReadonlyArray type, which is similar to Array except that all mutation methods are removed so that the Array cannot be overwritten after creation.
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12;
// error!
//Index signature in type 'readonly number[]' only permits reading.
ro.push(5);
// error!
//Property 'push' does not exist on type 'readonly number[]'.
ro.length = 100;
// error!
//Cannot assign to 'length' because it is a read-only property.
a = ro;
// error!
//The type 'readonly number[]' is 'readonly' and cannot be assigned to
//the mutable type 'number[]'.
Copy the code
On the last line of the code snippet you will notice that it is not even allowed to assign an array of Readonly attributes to a normal array. You can only force it to be overridden by making a new type declaration:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
a = ro as number[];
Copy the code
Readonly vs const: Const defines variables, readonly defines attributes.
Additional attribute checking
In our first example, objects with additional attributes also pass the interface check. Later we also looked at optional properties and so-called property packs.
But naively combining the two can be a mistake. Take our last example:
interface SquareConfig { color? : string; width? : number; } function createSquare(config: SquareConfig): { color: string; area: number } { return { color: config.color || "red", area: config.width ? config.width*config.width : 20 }; } let mySquare = createSquare({ colour: "red", width: 100 }); // Argument of type '{ colour: string; width: number; }' // is not assignable to parameter of type 'SquareConfig'. // Object literal may only specify known properties, // but 'colour' does not exist in type 'SquareConfig'. // Did you mean to write 'color'?Copy the code
Note that we passed the wrong attribute name for the parameter object at the end. In pure JS, such errors are not easily detected.
You can say this: this item is typed correctly because the width property is compatible, there is no existing color property, only a minor colour property.
But TS takes the position that there may be a Bug. Object literals get special treatment and too many attribute checks are made when they are assigned to other variables or passed as parameters. An error is returned if an object literal has any properties not contained in the target type.
Getting around these checks is actually quite simple. The simplest way is to use a type assertion;
Let mySquare = createSquare({width: 100, opacity: 0.5} as SquareConfig); // validCopy the code
But we recommend that you add a string index signature when you are sure that the target object has a valid and useful attribute, such as this:
interface SquareConfig {
color? : string;
width? : number;
[propName: string]: any
}
Copy the code
We’ll talk about index signatures later, but before we do, you’ll notice that the SquareConfig interface can have any number of attributes and there is no limit to the type of their value when the attribute name is not color or width.
The final way to get around these checks, perhaps a little surprisingly, is to assign the object to another variable: since squareOptions doesn’t do too much property checking, the compiler won’t give you an error.
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
Copy the code
The above method only works if the object has an attribute name that matches the one described in the interface. But if there are none, an error will be reported
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
//Type '{ colour: string; }' has no properties in common with type 'SquareConfig'.
Copy the code
Keep in mind that with code like the one above, you don’t have to bypass the check. For complex object literals with their own methods and states, you probably need these techniques, but a lot of extra attribute errors are actually bugs. This means that when you perform additional attribute type checking on things like option packs, you’ll need to modify some of the type declarations. In this case, if you allow createSquare to pass an object with both color and colour attributes, you should amend the SquareConfig definition to describe this.
Function types
Interfaces can describe a wide variety of JS objects. In addition, interfaces can be used to describe function types.
To use the interface to describe the object type, we add a call signature to the interface. This is a bit like a function declaring only the argument list and return value type. Each parameter needs to declare its attribute name and attribute value type.
interface SearcgFunc{
(source: string, subString: string): boolean;
}
Copy the code
Once defined, we can use this function type interface just like any other interface. Here we will demonstrate how to create a variable of function type and assign it a function value of the same type.
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string): boolean{
let result = source.search(subString); return result > -1;
}
Copy the code
In order for function types to perform type checking correctly, the names of the parameters do not need to match. The above example could also be written like this:
let mySearch: SearchFunc;
mySearch = function (src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
};
Copy the code
Check function arguments one at a time, checking each other for the type of each corresponding argument position. If you don’t want to specify a type at all, TypeScript context types can infer parameter types because function values are assigned directly to variables of type SearchFunc. Here, the return type of a function expression is implied by the value it returns (false and true in this case).
let mySearch: SearchFunc;
mySearch = function (src, sub) {
let result = src.search(sub);
return result > -1;
};
Copy the code
If a function expression returns a number or a string, the type checker will make an error indicating that the return type does not match the return type described in the SearchFunc interface.
let mySearch: SearchFunc;
mySearch = function (src, sub) {
// Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'.
// Type 'string' is not assignable to type 'boolean'.
let result = src.search(sub);
return "string";
}
Copy the code
Indexable type
Similar to how we use interfaces to describe function types, we can also describe types that can be “indexed to,” such as a[10], or ageMap[” Daniel “]. Indexable types have an index signature that describes the type we can index into an object and the corresponding return type when we index it. Here’s an example:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
Copy the code
In the example above, the StringArray interface has an index signature. This index signature states that when StringArray is indexed numerically, it will return a string.
There are two supported index signatures: String and number. In fact, TS can support both types of indexers, but the type returned from the numeric indexer must be a subtype of the type returned from the string indexer. This is because when a number is indexed, JavaScript actually converts the object to a string before indexing it to it. This means that indexing with 100 (a number) is the same as indexing with 100 (a string), so the two indexes need to be consistent.
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
// Numeric index type 'Animal' is not assignable to string index type 'Dog'.
[x: string]: Dog;
Copy the code
While string index signatures are a powerful way to describe “dictionary” patterns, they also enforce that all attributes match their return types. This is because a string index declares obj. Proterty can also be written as obj[‘propterty’]. In the following place, the type name does not match the type of the string index, so an error is returned:
interface NumberDictionary {
[index: string]: number;
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
//Property 'name' of type 'string' is not assignable to string index type 'number'.
}
Copy the code
For this you can change the way you declare the index variable value type:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Copy the code
Finally, you can use the readonly flag to prevent index signature property values from being overwritten:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
// Index signature in type 'ReadonlyStringArray' only permits reading.
Copy the code
Class types
Of course, classes can also inherit interfaces:
interface A{
currentTime: String;}
class now extends A{
currentTime: String = '2020-11-12'
constructor(a: number, b: string){
this.age = a;
// ...
}
}
Copy the code
In addition to class public variables, methods can also be type inherited. Note that class inheritance interfaces only affect public variables and methods of a class, not private variables.
interface instanceWay{ new (a: number, b: number); } class simple implements instanceWay{ c: number; constructor(a: number, b: number) { // ... }}Copy the code
The above class returns an error at compile time.
This is because when checking in a class, it checks only the instance side of the class, whereas the constructor function is static and TS skips it, so ts returns an empty match error because no other qualified functions in the class are checked. (Try it out for yourself and find that whatever function is defined in this mode seems to return a null match error.)
interface instanceWay{
new (a: number, b: number): instanceInterFace;
}
interface instanceInterFace{
tick(): void;
}
function createInstance(
constructors: instanceWay,
a: number,
b: number,
): instanceInterFace {
return new constructors(a, b);
}
class Again implements instanceInterFace{
constructor(h: number, t: number){
// ...
}
tick(){
console.log('bilibili')
}
}
Copy the code
I’ve always found instanceWay to behave strangely, not like a normal class interface; This looks more like an interface to a method. After all, new methods are on constructors, and class inheritance doesn’t explicitly check constructor.
let instance = createInstance(Again, 11, 12);
Copy the code
That way we can correctly check if the constructor of the class conforms to the strange new type.
Alternatively, we can call:
const obj: instanceWay= class Again implements instanceInterFace{ constructor(h: number, m: number) {} tick() { console.log("beep beep"); }};Copy the code
Interface development
Of course, interfaces can also inherit from interfaces.
interface Shape{
color: string;
}
interface Square extends Shape{
sideLength: number;
}
let square = {} as Square;
square.color = 'blue';
square.sideLength = 10;
Copy the code
An interface can inherit from multiple interfaces.
Interface Square extends Shape, Another{... }Copy the code
Mixed type
He can describe multiple types of interface creation in terms of its role. Since the function itself is an object (in JS), interface manipulation is also allowed.
interface Counter { (start: number): string; interval: number; reset(): void; extra: '11'; } function getCounter(): Counter{ let counter = function (start: number){} as Counter; counter.interval = 123; counter.reset = function(){}; counter.extra = '11'; return counter; } let c = getCounter(); c(10); c.reset(); C.i nterval = 5.0;Copy the code
Interface inheritance class
I was stupid.
If you think about it, there’s really nothing logically wrong with interface inheritance classes. In TS, an interface inheriting a class might be similar to inheriting an interface.
class A{ name: 'makabaka'; myName: string; age: number; sayHello(){ console.log('oh no'); } constructor($name: string, age: number){ this.myName = $name; this.age = age; }} interface APlus extends A{} class B implements APlus{Copy the code
Furthermore, if class A has private variables, the situation is different.
class A{
private name: any;
}
interface APlus extends A {
}
class B implements APlus{
private name: any;
}
// error TS2420: Class 'B' incorrectly implements interface 'APlus'.
// Types have separate declarations of a private property 'name'.
Copy the code
At this point you have to force yourself to be A subclass of A.
Class B extends A implements APlus{} class B extends A implements APlus{}Copy the code
The above.