TypeScript’s type system is powerful enough to allow you to build custom types to deal with complex scenarios. In this article, we’ll delving deeper into TypeScript type programming.

Type to capture

typeof

The typeof operator captures the types of variables and object attributes, as in the following example:

let foo = 123;
let bar: typeof foo;
bar = 456;
bar = '789'; // Cannot assign type "string" to type "number". ts(2322)

let o = {
  v: 123};let bar1: typeof o.v;
bar1 = 456;
bar1 = '789'; // Cannot assign type "string" to type "number". ts(2322)
Copy the code

When you compile the code above, the variable bar has the same type as foo and bar1 has the same type as O.v, both of number; Look at the following example:

const foo = 123;
let bar: typeof foo; // the type of 'bar' is the literal type: 123
bar = 456; // Cannot assign type "456" to type "123". ts(2322)
Copy the code

Compiling the above code, we get an error that we cannot assign type “456” to type “123”. This is because we set the variable foo to a constant, which is typed in TypeScript. Foo is of literal type 123. You cannot assign any other value to the bar variable. You can change this behavior by explicitly declaring the type of foo, as in the following code:

const foo: number = 123;
let bar: typeof foo;
bar = 456;
Copy the code

Compile again, now the compiler all the way green. This is also in the process of coding we often encounter small problems, know the reason, I believe you can effectively avoid and solve similar problems.

keyof

Object attribute names can be extracted using the keyof keyword, as in the following example:

interface Colors {
  red: string;
  blue: string;
}

let color: keyof Colors; / / color is of type 'red' | 'blue'
color = 'red'; // ok
color = 'blue'; // ok
color = 'anythingElse'; // ts(2322)
Copy the code

The above code, the type of the variable color were limited in ‘red’ | ‘blue’, so we can’t add value anythingElse to variable color.

The generic

By parameterizing types, we can abstract logic that has a common behavior across multiple types for code reuse. This article covers only type programming in TypeScript generics. See TypeScript generics for more information about generics.

extends

The extends keyword is used to determine parent types of two types (see another article on TypeScript type compatibility for a discussion of parent types), as in the following example:

type isSubTyping<Child, Par> = Child extends Par ? true : false;
type isSubNumber = isSubTyping<1.number>; // true
type isSubString = isSubTyping<'string'.string>; // true
Copy the code

Allocation condition type

An allocation condition type means that when the extends keyword is used to condition a type, if the input is a union type, the input is broken down into separate types for type operations. Take the following example:

type BooleanOrString = string | boolean;
type StringOrNumberArray<E> = E extends string | number ? E[] : E;

type BoolOrStringArray = StringOrNumberArray<BooleanOrString>; // boolean | string[]
Copy the code

The above code, BoolOrStringArray type to a Boolean | string [], this is because the types of rules according to the distribution condition, we will BooleanOrString split into string and Boolean, This is then matched in turn as an argument to StringOrNumberArray:

  • becausestring extends string | numbertrue, so returnstring[];
  • becauseboolean extends string | numberfalse, so returnboolean;
  • Finally, combine the return values of the previous two steps, and obtainstring[]|boolean.

Let’s look at the following example:

type BooleanOrStringType = BooleanOrString extends string | number ? BooleanOrString[] : BooleanOrString // string | boolean
Copy the code

In the code above, we define BooleanOrStringType by directly inlining the internal logic of StringOrNumberArray, rather than indirectly calling StringOrNumberArray

, BooleanOrStringType type into the string at the moment | Boolean, this is because the type is only effective in generic distribution condition, in the case of a non-generic, BooleanOrString will participate in operation as a whole.

infer

In type definitions (common with generic types), sometimes we need to infer a type from the type of the input parameter. The infer keyword can be used to identify the type variable to be inferred, such as the following type definition:

type ParamType<T> = T extends(... args: infer P) =>any ? P : T;
Copy the code

In the above code, we define a new type that automatically gets the parameter type ParamType

. From the definition of args: infer P) => any, we cannot know the specific type of parameter type P. Therefore, type P can be modified by infer to tell the compiler to automatically derive related types when the input parameter T meets the function definition rules. Take the following example:

interface Person {
 name: string;
}
type PersonFunc = (person: Person) = > void;
type A = ParamType<PersonFunc>; // [person: Person]
type B = ParamType<string>; // string
Copy the code

From the above code, we can infer that A is of type [person: person] and B is of type string.

The index sign

When defining a type, we can use index signatures when we cannot exhaust all the attributes of the type and do not want to define it as any:

interface Messages {
  [type: string] : {message: string;
  };
}
Copy the code

In the code above, we define the Messages attribute using [type: string] (type is just an identifier and can be replaced with whatever name you need). This way of defining type attributes is called index signatures. In this way, you solve both the problem of not being able to exhaust type attributes and the security degradation caused by using any. Take the following example:

let messages: Messages = {};

messages['one'] = { message: 'one message' };
messages['two'] = { msg: 'two message' }; {MSG: string; } "assign to type" {message: string; } ". Object literals can specify only known properties, and "MSG" is not of type "{message: string; } ". ts(2322)

console.log(messages['one'].message);
console.log(messages['two'].msg); // Type "{message: string; } "does not exist on the property" MSG ". ts(2339)
Copy the code

In the above code, we can set attributes for messages dynamically and safely type check the values of the attributes during setting and reading.

Index signatures give us a lot of benefits in type definitions, but we also need to consider compatibility with explicit members, as in the following example:

interface Foo {
  [key: string] :number;
  x: string; // Attribute "x" of type "string" cannot be assigned to index "number" of type "string". ts(2411)
}
Copy the code

In the above code, we declare a string index signature and an explicit member x. The compiled code will throw ts(2411) exception, which the compiler does to keep the type safe. There is no guarantee that the value of x converted to number will be the same as the value of x itself if x is accessed through an index.

Mapping type

Mapping types are based on the syntax of index signatures. Mapping types can be used to construct one type from another. Take the following example:

type AnyType<T> = {
  [key in keyof T]: any;
}
Copy the code

In the definition statement of AnyType

, we use in and keyof operators on the basis of index signature syntax. Because AnyType

can construct a new type T1 from the input parameter type T, AnyType

can be called image type. Take the following example:


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

type AnyPerson = AnyType<Person>;
Copy the code

Analyzing the above code, AnyPerson is equivalent to:

interface AnyPerson {
  name: any;
  age: any;
  address: any;
}
Copy the code

Note that in can only be used in a type alias. If used in an interface, an exception will be thrown, such as:

type PersonKeys = 'name' | 'age' | 'address';
interface AnyPerson {
  [key in PersonKeys]: any; // The computed attribute name in the interface must reference an expression of type text or "unique symbol". ts(1169)
};
Copy the code

In actual combat

In this section, we examine some common type definitions to get a feel for the power of the TypeScript type system.

Exclude

Exclude

We Exclude the member U of the subtype that is excluded from the parameter T:
,>

type Exclude<T, U> = T extends U ? never : T;
type A = Exclude<number | string.number>; // string
Copy the code

Exclude

in the above code, Exclude

takes advantage of the characteristics of the allocation condition type, and it can be inferred from the rule:
,>
,>

  • AThe type ofnever|string;
  • Due to theneverIt’s all typessubtypesAnd therefore can benever|stringDo type reduction, and you end up withAThe type ofstring.

ReturnType

We can get the ReturnType of a method by ReturnType

:

type ReturnType<T extends(... args:any) = >any> = T extends(... args:any) => infer R ? R : any;

type A = ReturnType<() = > string>; // string
Copy the code

In the code above, ReturnType

uses the infer keyword, and the A type can be inferred as string according to the rules.

Required

With Required

we can set all the properties of the input parameter T as Required:

type Required<T> = {
  [P inkeyof T]-? : T[P]; };interfacePerson { name? :string; age? :string;
}

type RequiredPerson = Required<Person>;
Copy the code

In the definition of Required

, we use the mapping type and use the – sign, – and? After the key value. The combination of represents the removal of optional attributes, so it can be inferred that the RequiredPerson type is equivalent to:

interface RequiredPerson {
  name: string;
  age: string;
}
Copy the code

Alternatively, we can remove the readonly attribute from the property by – :

type Changeable<T> = {
  -readonly [P in keyof T]: T[P];
};

interface ReadonlyPerson {
  readonly name: string;
  readonly age: string;
}

type ChangeablePerson = Changeable<ReadonlyPerson>;
Copy the code

Where a ChangeablePerson is equivalent to:

interface ChangeablePerson {
  name: string;
  age: string;
}
Copy the code

Pick

With Pick

we can fetch the specified key from the input parameter T and form a new type:
,>

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

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

type PersonWithoutAge = Pick<Person, 'name' | 'address'>;
Copy the code

In the definition of Pick

, the type of the input parameter K is constrained to be the subtype of the key of the input parameter T, and according to the rules of mapping type, it can be inferred that the result of PersonWithoutAge is equivalent to:
,>

interface PersonWithoutAge {
  name: string;
  address: string;
}
Copy the code

We’ve looked at some common tool types above, but TypeScript has many more built-in tool types that you can explore for yourself.

conclusion

In this article, we first introduce typeof and KEYof in type capture, then discuss extends, allocation condition type and infer in generics, and then explain index signature and mapping type. Finally, we further strengthen the knowledge through practical examples. This article provides you with a good understanding of TypeScript type programming. In the future, you will be able to easily define a type system that can handle a variety of complex scenarios to build strong and secure applications.

Refer to the link

  • Basarat. Gitbook. IO/typescript /…
  • Basarat. Gitbook. IO/typescript /…
  • www.typescriptlang.org/docs/handbo…