This week’s session will share some TypeScript typing knowledge by parsing several useful and interesting examples of TypeScript typing. It is also a summary of what I have learned in nearly 100 Type-challenges. Due to the limited time to share, there may be a lot of TS skills you don’t know are not mentioned.

What is type gymnastics

  • Higher-order functions: Pass in a function and return another function.
  • Higher-order components: Pass in one component and return another.
  • Higher-order types: Pass in a type and return another type.

In TypeScript, we can use Type to define complex types. Type can declare generic parameters that let users pass in types and return new types through a series of conversions. It’s easy to think of TypeScript types as functions in the type space:

type MyPartial<T> = {
  [K inkeyof T]? : T[K]; };interface Person {
  name: string;
  age: number;
}

type R = MyPartial<Person>;
/* type R = { name? : string; age? : number; } * /
Copy the code

Type gymnastics is the implementation of some high-level types with special functions

SimpleVue

Implement a type that implements TS type checking for the Vue Options API. In fact, demand can be divided into the following problems:

  1. Properties in computed and methods cannot be accessed in the data method
  2. This in computed accesses the attributes of data
  3. This in Methods accesses the properties of data and computed
  4. The value type of the this attribute in methods that accesses computed is the return value type of the method in computed
SimpleVue({
  data() {
    // @ts-expect-error
    this.firstname;
    // @ts-expect-error
    this.getRandom();
    // @ts-expect-error
    this.data();

    return {
      firstname: 'Type'.lastname: 'Challenges'.amount: 10}; },computed: {
    fullname() {
      return `The ${this.firstname} The ${this.lastname}`; }},methods: {
    getRandom() {
      return Math.random();
    },
    hi() {
      alert(this.amount);
      alert(this.fullname.toLowerCase());
      alert(this.getRandom()); ,}}});Copy the code

The this argument to the function

declare function SimpleVue(options: {
  //Function of thethisA parameter is a special parameter in the TS function that constrains the functionthistype//The statementthisThe parameter is of null type data: (this: {}) = >any;
}) :any;

SimpleVue({
  data() {
    // @ts-expect-error
    this.firstname;
    // @ts-expect-error
    this.getRandom();
    // @ts-expect-error
    this.data();

    return {
      firstname: 'Type'.lastname: 'Challenges'.amount: 10}; }});Copy the code

ThisType

ThisType is a built-in TypeScript utility type that marks the This type of a method in an object type.

Such as:

typeObjectDescriptor<D, M> = { data? : D; methods? : M & ThisType<D & M>;// Type of 'this' in methods is D & M
};

function makeObject<D.M> (desc: ObjectDescriptor<D, M>) :D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return{... data, ... methods }as D & M;
}

let obj = makeObject({
  data: { x: 0.y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // Strongly typed this
      this.y += dy; // Strongly typed this,}}}); obj.x =10;
obj.y = 20;
obj.moveBy(5.5);
Copy the code

Understanding TypeType allows us to solve the second and third problems.

Pattern matching

When writing complex types, we often need to use pattern matching to get the compiler to help us guess the subtypes of a type.

The PromiseValue type is a common and very simple implementation application for pattern matching.

type PromiseValue<P extends Promise<unknown>> = P extends Promise<infer V> ? V : never;
type V = PromiseValue<Promise<number> >;// => number
Copy the code

Notice that the implementation also uses conditional types and the Infer operator.

Conditional types allow the TS type space to have conditional control flow, using the form:

If A is A subtype of B, return C, otherwise return D
A extends B ? C : D
Copy the code

The infer operator is used to define a type variable in a pattern match whose specific type is inferred by the compiler from the pattern match.

With the function this argument mentioned earlier, we can use pattern matching to derive a function’s this type:

type GetThisType<F extends(... args:any[]) = >void> = F extends (
  this: infer TT, ... args:any[]
) => void
  ? TT
  : never;

declare function func(this: { name: string }) :void;
type TT = GetThisType<typeof func>;
/* type TT = { name: string; } * /
Copy the code

To solve the fourth problem, we need to be able to infer the return value type of a function. This can be easily done by asking the compiler to infer the return value type using pattern matching:

type GetReturnType<F extends(... args: unknown[]) => unknown> = Fextends (
  ...args: unknown[]
) => infer RT
  ? RT
  : never;

type RT = GetReturnType<() = > Awesome!>; / / = > 666
Copy the code

implementation

type GetReturnType<F extends(... args: unknown[]) => unknown> = Fextends (
  ...args: unknown[]
) => infer RT
  ? RT
  : never;

type GetComputed<C extends Record<string.any>> = {
  [K in keyof C]: GetReturnType<C[K]>;
};

declare function SimpleVue<D.C.M> (options: {
  data: (this: {}) => D;
  computed: C & ThisType<C & D>;
  methods: M & ThisType<M & D & GetComputed<C>>;
}) :any;

SimpleVue({
  data() {
    // @ts-expect-error
    this.firstname;
    // @ts-expect-error
    this.getRandom();
    // @ts-expect-error
    this.data();

    return {
      firstname: 'Type'.lastname: 'Challenges'.amount: 10}; },computed: {
    fullname() {
      return `The ${this.firstname} The ${this.lastname}`; }},methods: {
    getRandom() {
      return Math.random();
    },
    hi() {
      alert(this.amount);
      alert(this.fullname.toLowerCase());
      alert(this.getRandom()); ,}}});Copy the code

promiseAll

PromiseAll implements the same type declaration as promise. all, with proper parameters and return types:

const p1 = Promise.resolve(1);
const p2 = Promise.resolve(true);
const p3 = Promise.resolve('good! ');
const r = promiseAll([p1, p2, p3]);
Promise
      
Copy the code

The first version implements:

type PromiseValue<P extends Promise<unknown>> = P extends Promise<infer V> ? V : never;

declare function promiseAll<T extends readonly Promise<unknown> > [] (
  promises: T,
) :Promise<{
  readonly [P in keyof T]: T[P] extends Promise<unknown> ? PromiseValue<T[P]> : never; } >.const p1 = Promise.resolve(1);
const p2 = Promise.resolve(true);
const p3 = Promise.resolve('good! ');
const r = promiseAll([p1, p2, p3]);
// const r: Promise<readonly (string | number | boolean)[]>
Copy the code

You can see the value array type is inferred a readonly (string | number | Boolean) [], is not what we want readonly (number, Boolean, string).

Context type

This is mostly due to typescript types’ automatic derivation mechanism. For [P1, P2, P3], TSC by default will infer it into (Promise < number > | Promise < Boolean > | Promise < string >) []. One of the rules of TSC’s type inference design is that the default type inference is wide. For example, const n = 1 is not inferred to a literal type of 1.

In order for TSC to infer more narrowly, we need some additional modifier or markup to make TSC infer more narrowly.

For literal types, we know as const:

const obj = {
  name: 'ly',}as const;

/** // obj is inferred to {readonly name: "ly"; } * /
Copy the code

There are two common approaches to the promiseAll issue itself.

One way to do this is to deconstruct the array arguments in the form of arrays:

declare function promiseAll<T extends readonly Promise<unknown> > [] (
  // Write it as an array destruct so that the compiler recognizes T as a tuple
  promises: [...T],
) :Promise<{
  readonly [P in keyof T]: T[P] extends Promise<unknown> ? PromiseValue<T[P]> : never; } >.Copy the code

Another way is to combine an empty tuple with generic parameter constraints:

// T extends (readonly Promise<unknown>[]) | []
declare function promiseAll<T extends readonly Promise<unknown> > | [] [] (
  promises: T,
) :Promise<{
  readonly [P in keyof T]: T[P] extends Promise<unknown> ? PromiseValue<T[P]> : never; } >.Copy the code

The whole arrangement

We mentioned the use of conditional types to implement conditional control flow earlier, so let’s use type recursion to implement circular control flow through the full permutation example.

The effect we want to achieve:

type R1 = Permutation<'A' | 'B' | 'C'>;
// 3 x 2 x 1
// => "ABC" | "ACB" | "BAC" | "BCA" | "CAB" | "CBA"

type R2 = Permutation<'A' | 'B' | 'C' | 'D'>;
/ * / / should be 4 x 3 x 2 x 1 = 24 "ABCD" | "ABDC" | "ACBD" | "ACDB" | "ADBC" | "ADCB" | "BACD" | "BADC" | "BCAD" | | "BCDA"  "BDAC" | "BDCA" | "CABD" | "CADB" | "CBAD" | "CBDA" | "CDAB" | "CDBA" | "DABC" | "DACB" | "DBAC" | "DBCA" | "DCAB" | "DCBA" */
Copy the code

Template string type

We all know that TS has string literals, which are subtypes of string:

type S = '666'
// S is string literal type '666'

const s = '666';
// s is a string

'666' extends string  ? true : false; // => true
string extends '666' ? true : false; // => false
Copy the code

The template string type is a new type in typescript 4.1 that was implemented by Anders Hejlsberg, the father of C#, typescript and Delphi. In combination with pattern matching, type recursion and other features greatly enhance the playability of string types.

Prior to TS 4.1, the following code reported an error because there was no template string type:

function dateFormat(date: Date, formatStr: string, isUtc: boolean) {
  const getPrefix = isUtc ? 'getUTC' : 'get';
  // eslint-disable-next-line unicorn/better-regex
  return formatStr.replace(/%[YmdHMS]/g.(m: string) = > {
    let replaceStrNum: number;
    switch (m) {
      case '%Y':
        // Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Date'.
        //No index signature with a parameter of type 'string' was found on type 'Date'
        return String(date[`${getPrefix}FullYear`] ());// no leading zeros required
      case '%m':
        replaceStrNum = 1 + date[`${getPrefix}Month`] ();break;
      case '%d':
        replaceStrNum = date[`${getPrefix}Date`] ();break;
      case '%H':
        replaceStrNum = date[`${getPrefix}Hours`] ();break;
      case '%M':
        replaceStrNum = date[`${getPrefix}Minutes`] ();break;
      case '%S':
        replaceStrNum = date[`${getPrefix}Seconds`] ();break;
      default:
        return m.slice(1); // unknown code, remove %
    }
    // add leading zero if required
    return ` 0${replaceStrNum}`.slice(-2);
  });
}
Copy the code

The basic use

Using interpolation syntax, you can insert an existing string literal type, a numeric literal type into a string to get a new string literal type

type World = 'world';
type Greeting = `hello ${World}`; // => type Greeting = "hello world"
Copy the code

If the interpolation is never, then the entire template string returns never:

type N = `I The ${never} give up`; // => never
Copy the code

When the interpolation itself is of type Union, the result is also of type Union:

type Feeling = 'like' | 'hate';
type R = `I ${Feeling} you`; // => "I like you" | "I hate you"
Copy the code

If multiple unions are inserted, the result is a union of all combinations.

type AB = 'A' | 'B';
type CD = 'C' | 'D';
type Combination = `${AB}${CD}`; // => "AC" | "AD" | "BC" | "BD"
Copy the code

Application of template string type in pattern matching

For example, we want to implement a string statement that capitalizes the first letter of an incoming string statement:

type R1 = CapitalFirstLetter<'a little story'>; // => "A little story"
type R2 = CapitalFirstLetter<' '>; / / = > ""
Copy the code

We can do this:

type LetterMapper = {
  a: 'A';
  b: 'B';
  c: 'C';
  d: 'D';
  e: 'E';
  f: 'F';
  g: 'G';
  h: 'H';
  i: 'I';
  j: 'J';
  k: 'K';
  l: 'L';
  m: 'M';
  n: 'N';
  o: 'O';
  p: 'P';
  q: 'Q';
  r: 'R';
  s: 'S';
  t: 'T';
  u: 'U';
  v: 'V';
  w: 'W';
  x: 'X';
  y: 'Y';
  z: 'Z';
};

type CapitalFirstLetter<S extends string> = S extends `${infer First}${infer Rest}`
  ? First extends keyof LetterMapper
    ? `${LetterMapper[First]}${Rest}`
    : S
  : S;
Copy the code

Type recursive

For example, we want to implement a string that returns all characters in uppercase:

type R1 = UpperCase<'a little story'>; // => "A LITTLE STORY"
type R2 = UpperCase<'nb'>; // => "NB"
Copy the code

The recursion is:

Capitalize the first letter and recurse for the rest

Implementation is:

type UpperCase<S extends string> = S extends `${infer First}${infer Rest}`
  ? `${CapitalFirstLetter<First>}${UpperCase<Rest>}`
	// If S is an empty string, the branch will return the empty string
  : S;
Copy the code

Distributed computation of Union

In TypeScript distributed computing rules are triggered if the condition type extends to the left is a Union:

type Distribute<U> = U extends 1 ? 1 : 2;
// Unfamiliar people might think the return 2 is the false branch
type R = Test<1 | 2>; / / = > 1 | 2

/ / is equivalent to
type R1 = (1 extends 1 ? 1 : 2) | (2 extends 1 ? 1 : 2);
Copy the code

We can use Union extends Union to traverse each item of a Union:

// Declare an extra generic type E to identify the elements of the loop
type AppendDot<U, E = U> = E extends U ? `${E & string}. ` : never;
// Use Union to map
type R1 = AppendDot<'a' | 'b'>; // => "a." | "b."

// Use as to filter keys
type Getter<T> = {
    [P in keyof T as P extends `get${infer Rest}` ? P : never]: T[P];
};

const obj = {
    age: 18.getName() {
        return 'ly';
    },
    hello() {
        console.log('hello'); }};type R = Getter<typeof obj>;
/* type R = { getName: () => string; } * /
Copy the code

Determines whether a type is never

Implement a type IsNever to achieve this effect:

type R1 = IsNever<number>; // => false
type R2 = IsNever<never>; // => true
Copy the code

Some people think this is not easy, just use the condition type to check, brush write the following code:

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

type R1 = IsNever<number>; // => false
/ / scared
type R2 = IsNever<never>; // => never
Copy the code

The reason is that by default the semantics of never are empty union, and empty union extends returns never for any type. If you look at the TS source code, TS returns never when it sees the left side of extends.

Additional tags are needed to make TSC recognize never as a separate type:

// There are many ways to tag
type IsNever<T> = [T] extends [never]?true : false;
type IsNever<T> = T[] extends never[]?true : false;
type IsNever<T> = (() = > T) extends() = >never ? true : false;
Copy the code

The whole idea of permutation

From childhood to high school, we basically learn permutation and combination in math classes every year. In order to solve the problem of full permutation in type system, we can first think about how to achieve full permutation with JS code, and think about how to achieve full permutation when you brush Leetcode. TS type is just a means to implement logic, the key is the idea.

We can solve this problem recursively:

Permutation(n) = n * Permutation(n-1) = n * Permutation(n-1) = n * Permutation(n-1)

JS implementation looks like this:

function permutation(list) {
  if (list.length === 1) return [list[0]].const result = [];
  for (const [index, e] of list.entries()) {
    const rest = [...list];
    rest.splice(index, 1);

    for (const item ofpermutation(rest)) { result.push([e, ...item]); }}return result;
}

console.log(permutation(['a'.'b'.'c']));
/* [ 'a', 'b', 'c' ], [ 'a', 'c', 'b' ], [ 'b', 'a', 'c' ], [ 'b', 'c', 'a' ], [ 'c', 'a', 'b' ], [ 'c', 'b', 'a' ] */
Copy the code

TS implements full permutation

type Permutation<U, E = U> = [U] extends [never]?' '
  : E extends U
  ? `${E & string}${Permutation<Exclude<U, E>>}`
  : ' ';
Copy the code

Homework:

Click on the answer >
// From bottom up, use recursion loop
type Fibonacci<
    T extends number.// This array is used to take the length of the loop subscript
    TArray extends ReadonlyArray<unknown> = [unknown, unknown, unknown],
    // The length of the array is the value of the previous item
    PrePre extends ReadonlyArray<unknown> = [unknown],
    // represents the value of the previous item
    Pre extends ReadonlyArray<unknown> = [unknown],
> = T extends 1
    ? 1
    : T extends 2
    ? 1
    : TArray['length'] extends T // indicates that T times have been done
    ? [...Pre, ...PrePre]['length'] // Add the first two terms
    : Fibonacci<T, [...TArray, unknown], Pre, [...Pre, ...PrePre]>; // Use recursive loops
Copy the code

Effect:

// Fibonacci sequence: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144,
type R1 = Fibonacci<1>; / / = > 1
type R3 = Fibonacci<3>; / / = > 2
type R8 = Fibonacci<8>; / / = > 21
Copy the code