In addition to basic types, TypeScript defines many high-level types, including literal types, union types, cross types, index types, map types, conditional types, this types, and more. Because there is too much to cover, this article will cover the first three types, and the rest will be covered in the next article on advanced types.

Series of articles:

  • TS portal | why TypeScript study?
  • TS portal | explanation TypeScript data types
  • TS portal | explanation TypeScript enumerated types
  • TS portal | explanation TypeScript function types
  • TS portal | explanation TypeScript interface type
  • TS portal | explanation TypeScript class type

Literal types

In TypeScript, literals can represent not only values but also types, known as literal types. TypeScript supports three literal types: string literals, number literals, and Boolean literals. The corresponding string literals, numeric literals, and Boolean literals have the same literal type as their value:

let str: 'hello world' = 'hello world';
let num: 996 = 996;
let bool: true = true
Copy the code

1. Use of literal types

(1) String literal

String literals are string constants, unlike string literals, which are concrete values:

type Name = "TS";
const name1: Name = "test"; // Error cannot assign type "test" to type "TS"
const name2: Name = "TS";
Copy the code

In fact, defining a single literal type is not very useful. It is used when multiple literal types can be combined into a single union type to describe a practical set with explicit members:

type Direction = "north" | "east" | "south" | "west";
function getDirectionFirstLetter(direction: Direction) {
  return direction.substr(0.1);
}
getDirectionFirstLetter("test"); // Parameters of error type "test" cannot be assigned to parameters of type "Direction"
getDirectionFirstLetter("east");
Copy the code

Here we combine four string literal types into a single joint type, so that the compiler checks whether the parameter we use is a member of the specified set of literal types. In this way, you can restrict the parameters of a function to more specific types. This not only improves the readability of the code, but also ensures the parameter types of the function.

(2) numeric literals

Numeric literals are similar to string literals in that they are values of a specific type:

type Age = 18;
interface Info {
  name: string;
  age: Age;
}
const info: Info = {
  name: "TS".age: 28 // Error cannot assign type 28 to type 18
};
Copy the code

(3) Boolean literals

Boolean literals are similar to the two above, without further explanation:

let success: true
let fail: false
let value: true | false
Copy the code

Since booleans are only true and false, the following two types have the same meaning:

let value: true | false
let value: boolean
Copy the code

2. The broadening of literal types

In ES6, there are two new keywords for declaring variables: let and const. Are the types of variables the same when they define the same value?

Let’s start with an example of using const to define a variable:

const str = "hello world";
const num = 996;
const bool = false;
Copy the code

Const here defines three immutable constants, and TypeScript will infer that it is of the type of the assignment literal without a type annotation. So you can’t change the value of the variable anymore.

Another example of using let to define variables:

let str = "hello world";
let num = 996;
let bool = false;
Copy the code

The type of the variable that is not annotated becomes the parent of the assignment literal, for example STR is string, the parent of the string literal “Hello world”, num is number, the parent of the number literal 996, Bool is the parent of Boolean literal type false. This means that we can assign string, number, and Boolean values to these three variables:

str = "hello TypeScript";
num = Awesome!;
bool = true;
Copy the code

This design of converting a literal type to its parent is an extension of the literal type. A variable, function parameter, or non-read-only property of an object defined by let or var, if the initial value is specified and no explicit type annotation is added, then the inferred type is the type widened after the specified initial value literal type. This is called literal type broadening.

Here’s an example to understand literal type broadening:

let str = 'hello'; // The type is string
let strFun = (str = 'hello') = > str; // The type is (STR? : string) => string;


const specifiedStr = 'hello'; // The type is 'this is string'
let str2 = specifiedStr; // The type is 'string'
let strFun2 = (str = specifiedStr) = > str; // The type is (STR? : string) => string;

Copy the code

In the first code, let defines the string STR, which is a parameter, and does not explicitly declare its type, which is type widening, so the variable and parameter infer the type as string.

The string specifiedStr is a constant and cannot be modified. Therefore, specifiedStr is of type Hello literal. The following str2 traversal and strFun2 function parameters are assigned literal constants. There is no explicit declaration of type, so the types of variables and parameters are broadened and not specified as their corresponding literal types. This is in line with our expectations.

Second, the type of association

1. Use of union types

If you want to attribute is one of many types, such as a string or an array, then the joint types will come in handy, it USES | as markers, such as string | number). ** A union type can be understood as a union of multiple types. ** Union types are used to indicate that the type of a variable or parameter is not a single type, but may be a combination of different types:

function formatCommandline(command: string[] | string) {
  let line = ' ';
  if (typeof command === 'string') {
    line = command.trim();
  } else {
    line = command.join(' ').trim(); }}Copy the code

Joint types represent a value can be one of several types, with a vertical bar | separated for each type, so the number | string | Boolean said a value can be one of number, string, Boolean type.

You can use type aliases to extract union types:

type Command = string[] | string
Copy the code

2. Type reduction

Having said the basics of using union types, what happens if you define union types that include numeric and numeric literal types? In fact, because numeric types are the parent of numeric literal types, they are eventually reduced to numeric types. Likewise string and Boolean type reduction occurs in this case.

Look at the following example:

type UnionNum = 1 | number;  // The type is number
type UniomStr = "string" | string;  // The type is string
type UnionBool = false | boolean;   // The type is Boolean
Copy the code

In this case, TypeScript shrinks the type, removing the literal type and leaving the original type.

But there is a problem: the compiler can only tell us which original type we are defining:However, TypeScript provides a way to control type reduction by simply adding”& {}“You can:At this point, other literal types are not reduced, and string literals str1, str2, and so on are automatically suggested in the editor.

In addition, when the members of the union type are interface types, such that the attribute of one interface is a subset of the attribute of the other interface, the attribute is also type reduced:

type UnionInterface = {
  age: "18"
} | {
  age: "18" | "25",
  [key: string] :string;
}
Copy the code

Due to “18” is “18” | “25” subset, so the age attribute types become “18” | “25”.

3. Identify the union type

Singletons, union types, type protections, and type aliases can be combined to create a type called a recognizable union type, which can also be called a label union or algebraic data type.

Singleton types can be understood as data types that conform to the singleton pattern, such as enumerator types and literal types.

Identifiable union types require two elements:

  • Has ordinary singleton type attributes.
  • A type alias that contains the union of those types.

Identifiable union types are used to ensure that each case can be processed.

Here’s an example:

interface Square {
  kind: "square";    // Attributes that can be identified
  size: number;
}
interface Rectangle {
  kind: "rectangle"; // Attributes that can be identified
  height: number;
  width: number;
}
interface Circle {
  kind: "circle";   // Attributes that can be identified
  radius: number;
}

type Shape = Square | Rectangle | Circle; 
function getArea(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2; }}Copy the code

In the example above, our Shape can identify the union type, which is the union of three interfaces, and the three interfaces all have a kind attribute, and each interface has a different kind attribute value, which can be used to identify the type. The function should contain a case for each interface in the union type.

If the function does not contain a case for each interface in the union type. Hopefully, the compiler should give you a hint. There are two ways to check integrity: use strictNullChecks and use never.

(1) strictNullChecks

Add an interface to the above example:

interface Square {
  kind: "square";
  size: number;
}
interface Rectangle {
  kind: "rectangle";
  height: number;
  width: number;
}
interface Circle {
  kind: "circle";
  radius: number;
}
interface Triangle {
  kind: "triangle";
  bottom: number;
  height: number;
}
type Shape = Square | Rectangle | Circle | Triangle; 
function getArea(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2; }}Copy the code

In this case, the Shape union has four interfaces, but the function’s switch contains only three cases. The compiler does not give any error, because when the function is passed a Triangle and none of the cases match, the return statement is not executed. So the function returns undefined by default. StrictNullChecks: strictNullChecks: strictNullChecks: strictNullChecks: strictNullChecks: strictNullChecks: strictNullChecks

function getArea(s: Shape) :number {
  // error Function lacks ending return statement and return type does not include 'undefined'
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2; }}Copy the code

This method is simple, but does not support older code because strictNullChecks is a 2.0 configuration item and will not work if you use a version lower than that.

(2) Use the never type

When a function returns an error or no value is possible, the return value type is never. So you can add a default flow to the switch. If none of the previous cases match, the logic after default will be executed:

function assertNever(value: never) :never {
  throw new Error("Unexpected object: " + value);
}

function getArea(s: Shape) {
  switch (s.kind) {
    case "square":
      return s.size * s.size;
    case "rectangle":
      return s.height * s.width;
    case "circle":
      return Math.PI * s.radius ** 2;
    default:
      return assertNever(s); // Parameters of type Triangle cannot be assigned to parameters of type never}}Copy the code

With this approach, you need to define an additional asserNever function, but this approach not only indicates that the judgment condition is missing at compile time, but also raises an error at run time.

Three, cross type

1. Use of cross types

Cross typing is merging multiple types into one type. This allows us to superimpose existing types into a single type, which will have the characteristics of all member types. A cross type can be understood as the intersection of multiple types.

We can use the ampersand operator to declare an intersection type:

type Overlapping = string & number;
Copy the code

It makes no sense if we simply merge primitive, literal, function, and other atomic types into crossover types, because no variable can satisfy all of these types, and the type is essentially equal to never.

2. Usage scenarios of cross types

When should you use a cross type? Here’s a look at how the crossover type is used.

(1) Merge interface types

Combining multiple interface types into one type is a common use scenario for cross-typing. This is equivalent to implementing interface inheritance, known as merging interface types:

type Person = {
	name: string;
  age: number; } and {height: number;
  weight: number; } and {id: number;
}

const person: Person = {
	name: "zhangsan".age: 18.height: 180.weight: 60.id: 123456
}
Copy the code

Here we cross types so that Person has five of the three interface types at once.

So what happens if the same attribute in two interfaces defines different types?

type Person = {
	name: string;
  age: number; } and {age: string;
	height: number;
  weight: number;
}
Copy the code

Both interfaces have an age attribute and are of type number and string, respectively. After merging, age is of type string & number, which is a type of never:

type Person = {
	name: string;
  age: number; } and {age: string;
	height: number;
  weight: number;
}

const person: Person = {
	name: "zhangsan".age: 18.// Type 'number' is not assignable to type 'never'.ts(2322)
  height: 180.weight: 60,}Copy the code

If the attributes of the same name have compatible types, such as number, a subtype of number, or a numeric literal type, then the merged age attribute is a subtype of either:

type Person = {
	name: string;
  age: number; } and {age: 18;
	height: number;
  weight: number;
}

const person: Person = {
	name: "zhangsan".age: 20.// Type '20' is not assignable to type '18'.ts(2322)
  height: 180.weight: 60,}Copy the code

Here the age in the second interface is a numeric literal type, which is a subtype of number, so the merged type is literal type 18.

(2) Merge the joint type

Crossing types Another common use scenario is to merge union types. It is possible to merge multiple union types into a cross type that satisfies different union type constraints, that is, to extract members of the same type from all union types:

type A = "blue" | "red" | 996;
type B = 996 | Awesome!;
type C = A & B;
const c: C = 996;
Copy the code

If multiple union types do not have the same type member, then the crossed type is never:

type A = "blue" | "red";
type B = 996 | Awesome!;
type C = A & B;
const c: C = 996; // Type 'number' is not assignable to type 'never'.ts(2322)
Copy the code