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:
- Properties in computed and methods cannot be accessed in the data method
- This in computed accesses the attributes of data
- This in Methods accesses the properties of data and computed
- 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