TypeScript7 – intersection and index

Next, I will talk about the advanced types of TS. The so-called advanced types refer to some language features introduced by TS for the flexibility of the language. These features will help us deal with complex and changeable development scenarios. This article will talk about the intersection type and index type of TS.

1. Intersection type

1. Basic use of intersection types

The intersection type combines multiple types into one type, and the new type has the characteristics of all types, so the intersection type is especially suitable for the scene of object mixin (mixin).

interface Person {
    run(): void;
}

interface Teacher {
    goto(): void;
}

let active: Person & Teacher = {
    run() {},
    goto() {}
};

Here we define two interfaces Person and Teacher, the interface Person has a method run, and the interface Teacher has a method goto. We also define a variable active, whose type is the intersection type of interfaces Person and Teacher, and which has methods run and goto at the same time.

The intersection type is connected with ” & amp;”, and the variable active at this time should have the member methods owned by the two interface types. It should be noted here that although the name of the intersection type gives the impression that it is an alternation of types, it is actually a union of all types.

Next, let’s look at several types related to crossover types.

2. Joint type

Union types allow a variable to be of more than one type, with the properties of one of those types.

let n: string | number = 1;
n = 'abc';

Here we define a variable n whose type is the union type of string and number, and both 1 and ‘abc’ can be assigned to it.

The union type is connected by “|”, and the variable n at this time can have the characteristics of one of string and number types.

3. Literal type

Sometimes we not only want to limit the type of a variable, but also limit the value of this variable within a certain range, so we can use the literal type.

let m: 'm' | 2;
m = 'm';
m = 2;
m = 3; // report error

Here we define a variable m whose type is the union type of the literal type, which means that the value of m can only be ‘m’ or 2. If we assign a value of 3 to m, an error will be reported: Type ‘3’ is not assignable to type ‘”m” | 2’..

4. Object association type

Going back to the example in the previous article, we rewrote the toString method for both interfaces.

enum Type { obj, arr }
 
class IsObject {
    toObject() {
        console.log('hello object');
    }
    toString() {
        console.log('hello toString');
    }
}
 
class IsArray {
    toArray() {
        console.log('hello array');
    }
    toString() {
        console.log('hello toString');
    }
}
 
function getType(type: Type) {
    let target = type === Type.obj ? new IsObject() : new IsArray(); // union type
    return target;
}
 
getType(Type. obj);

The target here is a union type of IsObject and IsArray.

It needs to be mentioned here that if a variable is a joint type, it can only access the common members of all joint types if its type is not determined.

target.toObject(); // report an error
target.toString();

Therefore, when we use the target variable to call the toObject method, an error will be reported, but calling the toString method will not report an error.

At this time, an interesting thing happens. The union type seems to take the union of all types, but it can only access the intersection of all type members.

Summary: Intersection types are suitable for mixing objects, and union types can be indeterminate types, increasing the flexibility of code.

2. Index type

1. Scene introduction

In JS, we usually encounter this kind of scenario, such as getting the value of some attributes in an object, and then building a collection. Let’s implement this process.

let obj = {
    x: 1,
    y: 2,
    n: 3,
    m: 4
};

function getValue(obj: any, keys: string[]) {
    return keys. map(key => {
        return obj[key];
    });
}

console.log(getValue(obj, ['x', 'y'])); // [1, 2]
console.log(getValue(obj, ['a', 'b'])); // [undefined, undefined]

We define an object with some properties. A function getValue is also defined, which is used to obtain some attribute values in the object obj. If we go to get the properties x and y that exist in the object, we will get the correct result. But if you access properties a and b that do not exist in the object, undefined will be output and no error will be reported.

How to use TS to constrain this phenomenon? You can use the index type. To understand the index type, let’s first understand the related concepts of the index type.

2. Concepts related to index types

(1) Query operator of index type (keyof T)

“key of T” represents the literal union type of all public properties of type T. Give a simple example to illustrate:

interface Person {
    name: string;
    age: number;
}

let person: keyof Person; // 'name' | 'age'

We have defined an interface Person, which has two members name and age. We can use keyof to retrieve the name of the member of the interface Person. keyof Person is the joint type of the literal type ‘name’ and ‘age’.

(2) Indexed access operator (T[K])

The meaning of T[K] is the type represented by member K of interface T . Let’s look at another example:

interface Person {
    name: string;
    age: number;
}

let personProps: Person['age']; // number

Here we specify that the type of personProps is the type of Person.age, then the type of personProps is number.

3. T extends U

Indicates that generic variables can obtain certain properties by inheriting a certain type.

Next, let’s modify the getValue function.

First of all, let’s transform getValue into a generic function. We need to make some constraints. These constraints are that the elements in keys must be attributes of obj. How to do this constraint?

Let’s first write getValue as a generic function:

function getValue<T, K>(obj: T, keys: K[]) {
    return keys. map(key => {
        return obj[key];
    });
}

First, we define a generic variable T to constrain obj. Then a generic variable K is defined to constrain the keys array.

Here K must be a union type of literal type “‘x’ | ‘y’ | ‘n’ | ‘m'”, which is keyof T. Then let K inherit T to achieve the effect we want.

function getValue<T, K extends keyof T>(obj: T, keys: K[]) {
    return keys. map(key => {
        return obj[key];
    });
}

Finally, let’s set the return value type of the function getValue to T[K][]:

function getValue<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
    return keys. map(key => {
        return obj[key];
    });
}

T indicates the type of obj we passed in, and T[k][] indicates that the return value of getValue is an array, and the member type in the array must be the type of the attribute in obj.

Next, use the getValue function:

console.log(getValue(obj, ['x', 'y'])); // [1, 2]
console.log(getValue(obj, ['a', 'b'])); // report error

Getting the values of properties x and y outputs correctly. When we try to access the properties a and b that do not exist in the object obj, an error will be reported: Type ‘”a”‘ is not assignable to type ‘”x” | “y” | “n” | “m”‘. Type ‘”b”‘ is not assignable to type ‘”x” | “y” | “n” | “m”‘., indicating that attributes a and b do not exist in the object obj.

The index type can realize the query and return of object attributes, and with the generic constraints, we can use objects or object attributes and some constraint relationships between attribute values.