For starters, TypeScript might just add a type to the various variables we define and it doesn’t really change that much compared to JavaScript. But often encounter type hinting is not correct, the problem of inaccurate, flexible application and in the process of writing can’t, always copy to copy, deep understanding on after, personal feel generics is the essence of the TypeScript, but it’s not a simple type of reuse, flexible application to, but few on the market about this tutorial, The documentation is also vague.

I happened to find a generic programming exercise on the Internet, and after doing all the simple and medium topics above, I felt that generics are very similar to ordinary grammar, so it might be better to look at generics as a programming language from this point of view.

The keyword

keyof

Keyof is used to iterate over the key name of the interface type, converting the interface type to the union type

interface Type {
  name: string;
  age: number;
  sex: string;
}

type Person = keyof Type;

// p can only take the key of the interface, i.e. name or age
const p:Person = 'name';
// const p:Person = 'age'
Copy the code

in

The in keyword is used to iterate over union types, usually only within interface types

type Union = 'name' | 'age' | 'sex';

type ForEach = {
  [K in Union]: K
};
// The result looks like this
/ / {
// name: 'name',
// age: 'age',
// sex: 'sex'
// }
Copy the code

extends

The extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends extends

// When you use a generic T, and you want T to have some capabilities, such as array capabilities, string capabilities, and a property of an object
interface Animal {
  eat: (name: string) = > void
}

type Dog<T extends Animal> = {}; // Indicates that the type T passed to Dog must have the same property as Animal
Copy the code

infer

Infer is put at the end here because it is a TypeScript keyword and only used in generic programming. Literally, infer means inference and can be understood as the type of inference.

type Foo = (name: string) = > number;

type Param<T> = T extends (name: infer P) => any ? P : never;
type Return<T> = T extends(... args: unknown[]) => infer R ? R :never;

// This is an implementation of the built-in types Parameters
      
        and ReturnType
       
      
type Result = Param<Foo>; // string
type Result1 = Return<Foo>; // number

// Derive the array
type Arr = [1.'str'.false.Symbol];
type First<T> = T extends [infer F, ...infer Other] ? F : never;
type Last<T> = T extends [...infer Other, infer L] ? L : never;

type Result2 = First<Arr>; / / 1
type Result3 = Last<Arr>; // Symbol

// Derive the string
type Str = 'abcd';
type Capital<T extends string> = T extends `${infer F}${infer Other}` ? `${Uppercase<F>}${Other}` : T;

type Result4 = Capital<Str>; // Abcd
Copy the code

as

As is less commonly used in generic programming and is also used as a type assertion function, as shown in the following example

interface Foo {
  [key: string] :any;
  foo(): void;
}

type IndexSignature<T> = {
  [P in keyof T as string]: T[P]
};

type Result = IndexSignature<Foo>; // { [key: string]: any }

Copy the code

[key: string]; otherwise, all key-value pairs are selected.

The modifier –

Modifiers are modifiers that are added before the key name of an interface type, such as readonly, optional properties, and so on. In fact, TypeScript does mention it, but it doesn’t say what it’s called, so I took the liberty of using the word modifier in a programming language analogy.

To remove read-only and required modifiers like readonly and Required, use the “-” symbol, which I call the “reverse” modifier. Another thing not mentioned in the TypeScript documentation.

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

type CancelOnly<T> = {
  -readonly [K in keyof T]: T[K]
};

type Test = CancelOnly<Person>; // { name: string; age: nunber }

// For optional attributes, use the - symbol to deselect
interfacePerson1 { name? :string; age? :number;
}

type CancelOption<T> = {
  [K inkeyof T]-? : T[K] };type Test1 = CancelOption<Person1>; // { name: string; age: nunber }
Copy the code

Built-in types

Built-in types are types that exist in TypeScript and are equivalent to JavaScript types like Boolean that do not need to be defined. There are many built-in types. Here we will only explain the implementation of some common built-in types to improve your understanding of generic programming.

  1. Partial makes all keys in an interface type optional.
type MyPartial<T> = {
  [K inkeyof T]? : T[K] };Copy the code
  1. Required is the opposite of Partial, which makes all keys in the interface optional, whereas Required makes all keys mandatory, which removes them?The tag.
// The modifier mentioned above is used here -, deselecting
type MyRequired<T> = {
  [K inkeyof T]-? : T[K] }Copy the code
  1. Readonly, as the name implies, makes all keys in an interface type read-only, as well as implementation principles such as Required
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
};
Copy the code
  1. Pick

    Selects the key-value pairs specified by U from T to form a new type
    ,>
type MyPick<T, U extends keyof T> = {
  [K in U]: T[K]
};
Copy the code
  1. Exclude

    excludes U from T. Only union types can be used as T types. It can be interpreted as the reverse of the Pick type, but the type parameters are different.
    ,>
interface Person {
  name: string;
  age: number;
  height: number;
}

type MyExclude<T, U> = T extends U ? never : T;

// This will not work
type Test = MyExclude<Person, 'name'>;

// Pick is fine
type Test1 = MyPick<Person, 'name'>; // { name: string }

// Exclude changes T to a union type
type Test2 = MyExclude<keyof Person, 'name'>; / / the result is also joint types: 'age' | 'height, not we want to eliminate the effect of

// All keys can be traversed, and if the key inherits the given U type, it is excluded, leaving the remaining keys
type IExclude<T, U> = {
  [K in keyof T as K extends U ? never : K]: T[K]
}

type Test3 = IExclude<Person, 'name'>; // { age: number; height: number }
Copy the code
  1. Omit

    remove type U from T, which can be interpreted as the removal of specified keys from excuses
    ,>
interface Todo {
  title: string
  description: string
  completed: boolean
}

type MyOmit<T, U> = {
  [K in keyof T as K extends U ? never : K]: T[K]
}

type Test = MyOmit<Todo, 'description' | 'title'>; // { title: string }

// With the built-in types above, you can see that a key is excluded. This can be done by using Exclude
type MyOmit1<T, U> = Pick<T, Exclude<keyof T, 'description' | 'title'>>

// Exclude accepts a union type, so T needs to be converted
Copy the code

Custom type

The base type

Common type of foundation, string | number | Boolean | Symbol, there are some peculiar to TypeScript type, if any, unknown, never, void.

The complex type

  1. The interface type
  2. The joint type

Here we can think of our generics as functions. Identifiers such as T and K are actually our function parameters, and the concrete types that are actually passed in are the arguments.

// Take the built-in Pick type as an example
type Pick<T, U> = {
  [K in U]: T[K]
}

// It can be seen as this
// This is just an assumption to make it easier to understand generics
function Pick (T, U) {
  let res = {}
  for (let K in U) {
    res[K] = T[K]
  }
  return res
}
Copy the code

Type conversion

  1. The interface type changed to union

You can convert an interface type to a union type using the keyof keyword. To convert a union type to an interface type, the in keyword can be used. It has been mentioned above and will not be repeated here.

  1. Change the tuple type to another type
type Tuple = ['name'.'age'.'sex'];

// The tuple type is changed to the union type
type Tuple2Union<T extends unknown[]> = T[number];

// The tuple is converted to the object type
type TupleToObject<T extends any[]> = {
  [P in T[number]]: P
};

type resullt = TupleToObject<Tuple>;
/ / {
// a: "a";
// b: "b";
// c: "c";
// d: "d";
// }
Copy the code

Tuples can be converted to union types. How can union types be converted to tuples? In fact, after trying to find it is impossible to convert, after querying a lot of data, it is found that an explanation is more reasonable

A union type represents one of all types, while a tuple contains all types, and the relationship from or to and conflicts, so it cannot be converted.

  1. String type conversion to other types
// String type is converted to a tuple
// We also use type P, similar to the function default argument, to construct the tuple type
type String2Tuple<T extends string, P extends unknown[] = []> = 
  T extends `${infer F}${infer Rest}`
    ? String2Tuple<Rest, [...P, F]>
    : P

type Result = String2Tuple<'abc'>; // ['a','b','c']

// We can also convert a string to a union type by combining the tuple to union type method above
type String2Union<T extends string> = String2Tuple<T>[number]
Copy the code

Process control

priority

Logical relationship

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

interface Height {
  height: number;
  face: string;
  name: string;
}

type And<T, U> = T & U;

type Or<T, U> = T | U;

type PersonOfKeys = keyof Person;
type HeightOfKeys = keyof Height;

const case1: And<Person, Height> = {
  name: 'aaa'.age: 20.sex: 'female'.height: 170.face: '😂'
}; // The relationship between and

const case2: Or<Person, Height> = {name: 'aaaa'.age: 123.sex: 'vvv'}; // or relationship

const case3: And<PersonOfKeys, HeightOfKeys> = 'name'; // The element in the intersection

const case4: Or<PersonOfKeys, HeightOfKeys> = 'age'; // Set any element
Copy the code

traverse

  1. Traverse the interface

Use keyof to get the key name of the interface

  1. Iterate over the union type

Use the in keyword

Conversions of interface and union types

Interface to the union type is essentially the union type of the key name of the interface using keyof

If the union type is changed to interface, you can do this:

type ForEach<T extends keyof any> = {
  [K in T]: string
};

type Result = ForEach<Union>;
/ / {
// name: string,
// age: string,
// sex: string
// }
Copy the code
  1. Traverse the tuples

You can think of a tuple as an array, and iterating over an element is essentially fetching the value of each index in the tuple

type arr = ['a'.'b'.'c'.'d'];

// The element is an interface
/ / {
// 0: 'a',
// 1: 'b',
// 2: 'c',
// 3: 'd'
// }
// We can also use numbers to get the corresponding type
type Type = arr[0]; // 'a'

// Iterate over all numeric indexes, using number instead
// Note the generic constraint here, which means that T must inherit any array type in order to take the index of number
type ForEachTuple<T extends any[]> = T[number];

type Result = ForEachTuple<arr>; // 'a' | 'b' | 'c' | 'd'
Copy the code

The key to traversing tuples is to take the type number for each item in the element, and the result is a union type

Since tuples are understood as arrays, is it possible to get the length of a tuple?

type Length<T extends any[]> = T['length'];

type Result = Length<arr>; / / 4
Copy the code

conditional

type IsString<T> = T extends string ? true : false;

type Result = IsString<'abc'>; // true
Copy the code

This judgment of ternary expressions is easy to understand, but there are some special cases:

The first is to determine the type of never. How do you determine whether a type is never

type IsNever<T> = T extends never ? true : false;

type A = IsNever<never>; // never
Copy the code

The result is disappointing. This is still the “never” type. Why? I think the main reason is that the never type itself represents a non-existent type, which can be compared to the Error type. The occurrence of never indicates that the program has made an Error, so the following judgment cannot be made naturally.

If never is of type never, you can wrap it:

type IsNever<T> = [T] extends [never]?true : false; // true
Copy the code

Another tricky judgment, how do you know if a type is a union type

type IsUnion<T> = 
  T[] extends (T extends unknown ? T[] : never)?false
    : true;
Copy the code

T extends unknown? T[] : never, use an intermediate type to see what the type looks like

type Middle<T> = T extends unknown ? T[] : never;

type case = Middle<string>; // string[]
type case1 = Middle<number|string>; // number[]|string[]
type case2 = Middle<boolean>; // false[]|true[]
Copy the code

If T inherits from case, then the result will be true.

There is a special case, however, for Boolean types. The result of intermediate generics is the same as the union, so the final result is true.

So we need to make another judgment about Boolean types

type IsUnion<T> = 
  T[] extends (T extends unknown ? T[] : never)?false
    : T extends boolean
      ? false
      : true;
Copy the code

recursive

type Space = ' ' | '\n' | '\t';

// If there are Spaces in T, trim the remaining characters recursively
type Trim<T extends string> = T extends `${Space}${infer P}${Space}` ? Trim<P> : T;

type trimed = Trim<' Hello World '>; // 'Hello World'
Copy the code

Recursion is easy to understand, but it’s important to note that recursion in TypeScript is also a problem of recursion depth. Beyond a certain level, recursion will fail, so the need for performance is also an issue for generic programming.

// Note the recursion exit condition: the recursion stops when the length of type U is equal to the given length
// Add a number to the array each time you recurse
type ConstructTuple<T extends number, U extends number[] = []> = 
    U['length'] extends T
        ? U
        : ConstructTuple<T, [number. U]>type MinusOne<T extends number> = ConstructTuple<T> extends [infer _, ...infer Other] ? Other['length'] : 0;

type Zero = MinusOne<1> / / 0
type Five = MinusOne<5> / / 4
Type FourFive = MinusOne<45> type FourFive = MinusOne<45
Copy the code

The end of the

As for generic programming, you still need to practice it in your daily projects. Many generic builds have more than one way, and finding simpler, easy-to-understand generic builds is the goal of generic programming.

In the future, I will also put the 🌰 mentioned in the article into Github. Because of my limited knowledge, I welcome you to continue to supplement the better usage of generic programming.