Why

import * as fs from "fs";
function promisify(fn) {
  return function(. args) {
    return new Promise((resolve, reject) = >{ fn(... args,(err, data) = > {
        if(err) {
          returnreject(err); } resolve(data); }); }); }} (async() = > {let file = await promisify(fs.readFile)("./xxx.json"); }) ();Copy the code

The answer to this question will be given in the walkthrough, but if you thought it was easy, congratulations, you already know most of what this article is about.

  1. We need to knowpromisify(fs.readFile)(...)The type that is acceptable here.
  2. We need to knowlet file = await ...Here’s the type of file.

How to make a function like promisify retain type information is what gymnastics or what I call type computing is all about.

Introduction (Preface)

Yan Yan recently a popular term “TS gymnastics” has been used for TypeScript. “Gymnastics” is a joke for TC which is Turing complete. This is the practice of verifying that TypeScript type-level programming (compile time syntax) is Turing-complete and, when propagated, the practice of taking one TS Type as input and writing TS code to output another Type.

Erratum: The term gymnastics comes from the Haskell community, meaning difficult movements

TypeScript may already be turing-complete when validated:

  1. www.zhihu.com/question/41…

However, the author thinks that the above concept may be relatively small in the front circle, and the name “gymnastics” is relatively difficult for laymen to correspond with specific behaviors. At present, the whole TC process is more like an interesting Brain Teaser. Therefore, the author thinks that it will be easier to understand TC “gymnastics” by using Type Computing, Type Calculation or “Type Calculation”, which is also easy to correspond with specific behaviors. In the next part of this paper, “Type Calculation” will replace the term “gymnastics”.

Modeling (Modeling)

In essence, type computing is simply writing a program that takes a type as input and outputs another type, so it can be modeled as writing an ordinary program that classifies TS’s type-dependent syntax as part of a common computer language.

Grammar Classification

Take JS for example. From the point of view of AST (Abstract syntax tree), syntax can be classified according to the following hierarchy:

But we don’t do it in such a top-down tree structure today. The learning curve would be steep at first, so the author doesn’t do it in such a top-down order, but in the order of learning the grammar of ordinary languages.

Basic Types

Just as JS has basic types, TypeScript also has basic types. As you probably know, TypeScript has basic types:

  • Boolean
  • Number
  • String
  • Array
  • Tuple (TypeScript only)
  • Enum (TypeScript only)
  • Unknown (TypeScript only)
  • Any (TypeScript only)
  • Void (TypeScript only)
  • Null and Undefined
  • Never (TypeScript only)
  • Object

Any complex type is a combination of primitive types, each of which can have specific enumerations:

type A = {
    attrA: string.attrB: number.attrA: true.// Enumeration of Boolean. }Copy the code

Function (Function)

Let func = (argA, argB…) => expression;

While Javascript has the concept of functions, does TypeScript type-level Programming syntax have the concept of functions? The answer is yes, a type with a stereotype is equivalent to a function.

// Function definition
type B<T> = T & {
    attrB: "anthor value"
}

/ / variable
class CCC {... }type DDD = {
...
}

// Function call
type AnotherType = B<CCC>;
type YetAnotherType = B<DDD>;
Copy the code

Where

corresponds to the function parenthesis and argument list, and = corresponds to the function definition. Or along the same lines you can start depositing lots of utility class TC functions, for example

// Make all attributes optional
type Optional<T> = {
  [key inkeyof T]? : T[key]; }// Make some attributes mandatory
type MyRequired<T, K extends keyof T> = T &
  {
    [key inK]-? : T[key]; };For example, we have an entity
typeApp = { _id? :string;
  appId: string;
  name: string;
  description: string;
  ownerList: string[]; createdAt? :number; updatedAt? :number;
};

// When we update the object/type, some keys are mandatory and some are optional, so we can generate the type we need in this way
type AppUpdatePayload = MyRequired<Optional<App>, '_id'>
Copy the code

This example illustrates another analogous concept, which is that the type of a function parameter can be expressed using a

syntax.

Defects of TypeScript functions

At present the author of the following three defects have not found a way to overcome, smart you can try to see if there is a way to overcome.

Higher versions support recursion

Recursion was only supported in 4.1.0

Functions cannot be arguments

In JS, a function can be an input parameter to another function, for example:

function map(s, mapper) { return s.map(mapper) }
map([1.2.3].(t) = > s);
Copy the code

However, in the “function” of type evaluation, there is no syntax for passing functions as arguments. Correctly, the arguments passed can only be referred to as statically valued variables, not as callable functions.

type Map<T, Mapper> = {
  [k in keyof T]: Mapper<T[k]>; // Syntax error
}
Copy the code
Closures are supported, but there is no way to modify values in closures

I have found no alternative syntax “in functions” of TypeScript

type ClosureValue = string;

type Map<T> = {
  [k in keyof T]: ClosureValue; // I found no syntax to modify ClosureValue
}
Copy the code

But we can combine new types using concepts similar to functional programming.

type ClosureValue = string;

type Map<T> = {
  [k in keyof T]: ClosureValue & T[k]; // I found no syntax to modify ClosureValue
}
Copy the code

Statements (Statements)

In TypeScript, statement-related grammars seem to correspond only to variable declaration grammars. In TypeScript, there are no conditional statements, loop statement functions, or special function declaration statements (represented by the variable declaration statements below).

Variable Declaration

Analogy: let a = Expression;

Variable declarations are described in the introduction above, simply by using variable names such as type ToDeclareType = Expresion and the syntax of expressions. There are many kinds of expressions, and we’ll go into that in more detail,

type ToDeclareType<T> = T extends (args: any) => PromiseLike<infer R> ? R : never; // Conditional expressions/conditional expressions with ternary operators
type ToDeclareType = Omit<App>; // Function call expression
type ToDeclareType<T>= { // loop expression
    [key in keyof T]: Omit<T[key], '_id'>}Copy the code

Expressions (Expressions)

Conditional expression with ternary operator

Analogy: A == B? ‘hello’ : ‘world’;

When we write “conditional expressions with ternary operators” in JS, we usually say Condition, okay? ExpressionIfTrue: ExpressionIfFalse forms such as ExpressionIfTrue are expressed in TypeScript with the following syntax:

type TypeOfWhatPromiseReturn<T> = T extends (args: any) => PromiseLike<infer R> ? R : never;
Copy the code

T extends (args: any) => PromiseLike

is a fairly conditional judgment, and R: never is equivalent to expressions when true and expressions when false.

Using the ternary expression above, we can extend ReturnType to support asynchronous and synchronous functions

async function hello(name: string) :Promise<string> {
  return Promise.resolve(name);
}
// type CCC: string = ReturnType<typeof hello>; doesn't work
type MyReturnType<T extends(... args) =>any> = T extends (
  ...args
) => PromiseLike<infer R>
  ? R
  : ReturnType<T>;
type CCC: string = MyReturnType<typeof hello>; // it works
Copy the code

Function calling/defining expressions (CallExpression)

Analogy: call(a, B, C);

This was introduced in the “functions” section above

Loop Related (Object.keys, array.map, etc.)

For (let k in b) {… }

Loop Implementation (Details Explained)

There is no complete syntax for loops in TypeScript. Loops are implemented recursively. Here is an example:

Note: Recursion is only supported in TS 4.1.0

type IntSeq<N, S extends any[] = []> =
    S["length"] extends N ? S :
    IntSeq<N, [...S, S["length"]]>
Copy the code

In theory, the following are examples of function definitions/expressions, but object traversal is common enough to complete loop statements that it is worth mentioning separately.

Loop Object
type AnyType = {
  [key: string] :any;
};
type OptionalString<T> = {
  [key inkeyof T]? :string;
};
type CCC = OptionalString<AnyType>;
Copy the code
Loop Array/Tuple
map

Analogy: Array. The map

const a = ['123'.1{}];type B = typeof a;
type Map<T> = {
  [k in keyof T]: T[k] extends(... args) =>any ? 0 : 1;
};
type C = Map<B>;
type D = C[0];
Copy the code
reduce

Analogy: Array. Reduce

const a = ['123'.1{}];type B = typeof a;
type Reduce<T extends any[]> = T[number] extends(... arg:any[]) = >any ? 1 : 0;
type C = Reduce<B>;
Copy the code

Note that reduce returns a Union type.

Member Expression

The main reason we use member expressions such as A.B.C in JS is because we know the structure of an object/variable and want to retrieve the value of some part of it. In TypeScript, a common way to infer is by using the infer syntax. For example, we can infer function parameters:

function hello(a: any, b: string) {
  return b;
}
type getSecondParameter<T> = T extends (a: any.b: infer U) => any ? U : never;
type P = getSecondParameter<typeof hello>;
Copy the code

T extends (a: any, B: infer U) => any means to represent structures and take parts of them.

Of course, TypeScript itself has some simpler syntax

type A = {
  a: string;
  b: string;
};
type B = [string.string.boolean];
type C = A['a'];
type D = B[number];
type E = B[0];
// eslint-disable-next-line prettier/prettier
type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;
type F = Last<B>;
Copy the code

Common Datastructures and Operations

Set

Collection data structures can be replaced with Union types

Add
type S = '1' | 2 | a;
S = S | 3;
Copy the code
Remove
type S = '1' | 2 | a;
S = Exclude<S, '1'>;
Copy the code
Has
type S = '1' | 2 | a;
type isInSet = 1 extends S ? true : false;
Copy the code
Intersection
type SA = '1' | 2;
type SB = 2 | 3;
type interset = Extract<SA, SB>;
Copy the code
Diff
type SA = '1' | 2;
type SB = 2 | 3;
type diff = Exclude<SA, SB>;
Copy the code
Symmetric Diff
type SA = '1' | 2;
type SB = 2 | 3;
type sdiff = Exclude<SA, SB> | Exclude<SB, SA>;
Copy the code
ToIntersectionType
type A = {
  a: string;
  b: string;
};
type B = {
  b: string;
  c: string;
};
type ToIntersectionType<U> = (
  U extends any ? (arg: U) = > any : never
) extends (arg: infer I) => void
  ? I
  : never;
type D = ToIntersectionType <A | B>;
Copy the code
ToArray

Note: Recursion is only supported in TS 4.1.0

type Input = 1 | 2;
type UnionToIntersection<U> = (
  U extends any ? (arg: U) = > any : never
) extends (arg: infer I) => void
  ? I
  : never;
type ToArray<T> = UnionToIntersection<(T extends any ? (t: T) = > T : never) >extends(_ :any) => infer W
  ? [...ToArray<Exclude<T, W>>, W]
  : [];
type Output = ToArray<Input>;
Copy the code

Note: It may be a BUG in TS that makes this feature successful, because:

type C = ((arg: any) = > true) & ((arg: any) = > false);
type D = C extends (arg: any) => infer R ? R : never; // false;
Copy the code

But logically, C should be never, because you can’t find a function that always returns true and always false.

Size
type Input = 1 | 2;
type Size = ToArray<Input>['length'];
Copy the code

Map/Object

Merge/Object.assign
type C = A & B;
Copy the code
Intersection
interface A {
  a: string;
  b: string;
  c: string;
}
interface B {
  b: string;
  c: number;
  d: boolean;
}
type Intersection<A, B> = {
  [KA in Extract<keyof A, keyof B>]: A[KA] | B[KA];
};
type AandB = Intersection<A, B>;
Copy the code
Filter
type Input = { foo: number; bar? :string };
type FilteredKeys<T> = {
  [P in keyof T]: T[P] extends number ? P : never;
}[keyof T];
type Filter<T> = {
  [key in FilteredKeys<T>]: T[key];
};
type Output = Filter<Input>;
Copy the code

Array

Member access
type B = [string.string.boolean];
type D = B[number];
type E = B[0];
// eslint-disable-next-line prettier/prettier
type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;
type F = Last<B>;
type G = B['length'];
Copy the code
Append
type Append<T extends any[], V> = [...T, V];
Copy the code
Pop
type Pop<T extends any[]> = T extends [...infer I, infer _] ? I : never
Copy the code
Dequeue
type Dequeue<T extends any[]> = T extends [infer _, ...infer I] ? I : never
Copy the code
Prepend
type Prepend<T extends any[], V> = [V, ...T];
Copy the code
Concat
type Concat<T extends any[], V extends any[] > = [...T, ...V];
Copy the code
Filter

Note: Recursion is only supported in TS 4.1.0

type Filter<T extends any[]> = T extends [infer V, ...infer R]
  ? V extends number
    ? [V, ...Filter<R>]
    : Filter<R>
  : [];
type Input = [1.2.string];
type Output = Filter<Input>;
Copy the code
Slice

Note: For simplicity, Slice is used differently than array. Slice: N indicates the number of remaining elements.

type Input = [string.string.boolean];
type Slice<N extends number, T extends any[]> = T['length'] extends N
  ? T
  : T extends [infer _, ...infer U]
  ? Slice<N, U>
  : never;
type Out = Slice<2, Input>;
Copy the code

Slice (s, e) involves subtraction, which is a bit of a drag.

Operator (Operators)

Note: The implementation of the operators involves recursion, which is only supported in TS 4.1.0. Note: The following operators only work with integer types. Note: The principle relies on recursion and is inefficient

Basic Principles (Details Explained)

The basic principle is to print integers from the length property of the Array. If you want to implement the * method, add N times.

type IntSeq<N, S extends any[] = []> =
    S["length"] extends N ? S :
    IntSeq<N, [...S, S["length"]] >;Copy the code

= = =

type IfEquals<X, Y, A = X, B = never> = (<T>() = > T extends X ? 1 : 2) extends <
  T
>() = > T extends Y ? 1 : 2
  ? A
  : B;
Copy the code

+

type NumericPlus<A extends Numeric, B extends Numeric> = [...IntSeq<A>, ...IntSeq<B>]["length"];
Copy the code

Note: negative numbers are not supported by subtraction results…

type NumericMinus<A extends Numeric, B extendsNumeric> = _NumericMinus<B, A, []>;type ToNumeric<T extends number> = T extends Numeric ? T : never;type _NumericMinus<A extends Numeric, B extends Numeric, M extends any[]> = NumericPlus<A, ToNumeric<M["length"] > >extends B ? M["length"] : _NumericMinus<A, B, [...M, 0>;Copy the code

Other (MISC)

inferface

You might ask which of the above categories interface syntax belongs to, but instead of declaration-merging, interfaces are functioned by type. Interfaces are more like syntax candies. Therefore, the author does not use interface to implement any of the above functions.

inteface A extends B {
    attrA: string
}
Copy the code
Utility Types

TypeScript also provides utility types, such as Parameters, for fetching functions. For details, see this link.

Physical Exercise (Excercise)

Promisify

import * as fs from "fs";
function promisify(fn) {
  return function(. args: XXXX) {
    return new Promise<XXXX>((resolve, reject) = >{ fn(... args,(err, data) = > {
        if(err) {
          returnreject(err); } resolve(data); }); }); }} (async() = > {let file = await promisify(fs.readFile)("./xxx.json"); }) ();Copy the code
  1. We need to knowpromisify(fs.readFile)(...)The type that is acceptable here.
  2. We need tolet file = await ...Here’s the type of file.
The answer

Combined with type calculation and the new version of TS, it is much cleaner and more extensible than the official implementation library (only supports 5 parameters) github.com/DefinitelyT…

import * as fs from "fs";
// Basic data-based operations Last and Pop
type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;
type Pop<T extends any[]> = T extends [...infer I, infer _] ? I : never;
// Operate on arrays
type GetParametersType<T extends(... args:any) = >any> = Pop<Parameters<T>>;
type GetCallbackType<T extends(... args:any) = >any> = Last<Parameters<T>>;
// Similar to member variable values
type GetCallbackReturnType<T extends(... args:any) = >any> = GetCallbackType<T> extends (err: Error.data: infer R) => void ? R : any;
function promisify<T extends (. args:any) = >any> (fn: T) {
  return function(. args: GetParametersType
       ) {
    return new Promise<GetCallbackReturnType<T>>((resolve, reject) = >{ fn(... args,(err, data) = > {
        if(err) {
          returnreject(err); } resolve(data); }); }); }} (async() = > {let file = await promisify(fs.readFile)("./xxx.json"); }) ();Copy the code

MyReturnType

Basically the generic implementation of extracting a part as mentioned in the member expressions section (use the infer keyword)

const fn = (v: boolean) = > {
  if (v) return 1;
  else return 2;
};
type MyReturnType<F> = F extends(... args) => infer R ? R :never;
type a = MyReturnType<typeof fn>;
Copy the code

Readonly 2

Basically Merge and iterate over objects

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}
type MyReadonly2<T, KEYS extends keyof T> = T &
  {
    readonly [k in KEYS]: T[k];
  };
const todo: MyReadonly2<Todo, 'title' | 'description'> = {
  title: 'Hey'.description: 'foobar'.completed: false}; todo.title ='Hello'; // Error: cannot reassign a readonly property
todo.description = 'barFoo'; // Error: cannot reassign a readonly property
todo.completed = true; // O
Copy the code

Type Lookup

Member access and application of ternary expressions

interface Cat {
  type: 'cat';
  breeds: 'Abyssinian' | 'Shorthair' | 'Curl' | 'Bengal';
}
interface Dog {
  type: 'dog';
  breeds: 'Hound' | 'Brittany' | 'Bulldog' | 'Boxer';
  color: 'brown' | 'white' | 'black';
}
type LookUp<T, K extends string> = T extends { type: string}? T['type'] extends K
    ? T
    : never
  : never;
type MyDogType = LookUp<Cat | Dog, 'dog'>; // expected to be `Dog`
Copy the code

Get Required

See the Filter method of Object

type GetRequiredKeys<T> = {
  [key inkeyof T]-? : {}extends Pick<T, key> ? never : key;
}[keyof T];
type GetRequired<T> = {
  [key in GetRequiredKeys<T>]: T[key];
};
type I = GetRequired<{ foo: number; bar? :string} >.// expected to be { foo: number }
Copy the code

Thoughts (Thoughts)

Supplementary Utility Types

Add generic, easy-to-understand TypeScript Utility classes library to do TS underscore in addition to Utility Types.

Update: find a library like this already exists:

  • Github.com/piotrwitek/…
  • Github.com/sindresorhu…

Doing Type Computing in Plain TS

Even as modeled in this article, the above categorization shows that there are still a number of key capabilities missing compared to modern programming languages. The reason why type computing is so expensive to learn and is like a puzzle game is also because of the lack of grammatical elements and the unintuitive use of it. In order to make type computation available to a wider audience, a friendlier syntax, a more comprehensive syntax should be provided. The naive idea is to run a syntax (macros?) similar to JS itself in Compile Time. .

The following syntax is a slap in the face, for example:

type Test = {
    a: string
}
typecomp function Map(T, mapper) {
    for (let key of Object.keys(T)) {
        T[key] = mapper(T[key]);       
    }
}
typecomp AnotherType = Map(Test, typecomp (T) => {
    if (T extends 'hello') {
        return number;
    } else {
        return string; }});Copy the code

Having such an intuitive syntax, I feel, would make type computation much easier to get started with. To achieve this effect, we may need to fork the TypeScript repo and add the above functionality. Hopefully, competent readers can implement this capability in a high-quality way, or merge it into the source TypeScript repo. Benefit the author of this moment for the type of computing agonized developers.

Reference

  1. Github.com/type-challe…
  2. www.zhihu.com/question/41…
  3. Github.com/piotrwitek/…

❤️ Thank you

  1. Don’t forget to share, like and bookmark your favorite things.