TypeScript extends JavaScript’s syntax for typing. We can add types to variables, do type checking at compile time, and use the editor to make intelligent hints more accurate. In addition, TypeScript supports advanced types to add flexibility to the type system.
Just as JavaScript higher-order functions are functions that generate functions and React higher-order components are components that generate components, Typescript higher-order types are types that generate types.
TypeScript advanced types are types defined by type with type parameters (also called generics) that perform a series of type calculations on the type parameters passed in to generate new types.
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Copy the code
For example, the Pick is an advanced type that takes type parameters T and K, and the type parameters go through a series of type calculation logic that returns the new type.
TypeScript advanced types evaluate new types based on type parameters, a process that involves a set of type calculation logic called type gymnastics. Of course, this is not a formal concept, just a community joke, because some types of computation logic is more complex.
TypeScript’s type system is Turing-complete, which means it can describe any computable logic, which simply means loops, conditional judgment, and so on.
Since TypeScript’s type system is so strong, let’s do some type gymnastics for advanced types to get a feel for it.
We do these exercises:
- Add with ts type
- Generates a string repeated N times with type TS
- Simple JS Parser with TS type (part)
- Ts type to achieve object attributes by conditional filtering
I divide these gymnastics into the number class, the string class, the object class, the law of these three types of calculation logic to master, I believe your gymnastics level will improve a section.
TypeScript type syntax basics
Before doing some gymnastics, take a look at TypeScript’s type syntax, which means what type calculation logic can be done.
We have all the syntax we need, so let’s look at how to do loops and judgments:
Ts type of conditional judgment
The syntax for ts type conditional judgments is conditional? Branch 1: branch 2.
The extends keyword is used to determine whether A is of type B. In this example, the type argument T passed in is 1, which is of type number, so the final return is true.
Ts type loop
The TS type does not have loops, but you can loop recursively.
To construct an array of length N, we pass in Len for the length, Ele for the element, and Arr (for recursion) for the constructed array.
The type evaluation logic then determines whether the length of the Arr is Len. If so, it returns the constructed Arr. If not, it adds an element to the Arr to continue construction.
So we recursively create an array of length Len.
String operations of type TS
Ts supports constructing new strings:
It also supports fetching parts of a string based on pattern matching:
Since STR matches the pattern of AAA, we can put the right part into the local type variable declared by infer and return the value of the local variable.
Object operations of type TS
Ts supports object attributes and values:
You can also create new object types:
The new object type newObj is generated by fetching all the property names of obj through keyof, iterating through the property names and taking the corresponding property values through in.
We looked at common TS syntax, including conditional judgments, loops (implemented recursively), string operations (constructing a string, fetching a numerator), and object operations (constructing an object, fetching an attribute value). So let’s do some exercises with these.
Ts type gymnastics practice
Let’s divide gymnastics into three classes to practice, and then sum up the rules respectively.
Number of types of gymnastics
Gymnastics 1: implement advanced type Add, can do number addition.
Can TS add numbers? Of course it can, because it’s Turing complete, which means it can be done with all kinds of computable logic.
So how do you do that?
The array type can take the length property, which is just a number. You can add by constructing arrays of a certain length.
We have implemented an advanced type that builds a new array of a certain length recursively:
type createArray<Len, Ele, Arr extends Ele[] = []> = Arr['length'] extends Len ? Arr : createArray<Len, Ele, [Ele, ...Arr]>
Copy the code
You just construct two arrays of different lengths, merge them together, and take length.
type Add<A extends number, B extends number> = [...createArray<A, 1>, ...createArray<B, 1>] ['length']
Copy the code
Let’s test it out:
We add by constructing arrays!
The advanced types of TS can only perform numeric operations by constructing arrays of different lengths and then taking length, because there are no type addition, subtraction, multiplication and division operators.
String class gymnastics
Exercise 2: Repeat the string n times.
${A}${B} ${B} ${A}${B}
Counting involves number manipulation by constructing an array and taking length.
So, we’re going to recursively construct the array to count, and recursively construct the string, and then return the constructed string if the array length reaches the target.
So there are four type arguments Str (the string to be repeated), Count (the number of repeats), Arr (the array to Count), and ResStr (the constructed string) :
type RepeactStr<Str extends string,
Count,
Arr extends Str[] = [],
ResStr extends string = ' '>
= Arr['length'] extends Count
? ResStr
: RepeactStr<Str,Count, [Str, ...Arr], `${Str}${ResStr}`>;
Copy the code
We recursively construct the array and string. If the length of the constructed array reaches Count, we return the constructed string ResStr, otherwise we continue the recursive construction.
Test it out:
Summary: When constructing the string recursively, we need to recursively construct the array to do the counting until the counting condition is satisfied, and then the target string is generated.
This exercise just involves constructing strings, not getting substrings by pattern matching, so let’s do another exercise.
Exercise 3: implement simple JS Parser to parse function names and parameters of string add(11,22)
Parsing a string requires fetching substrings based on pattern matches. FunctionName, brackets, num and comma are to be resolved respectively. We implement the corresponding advanced types respectively.
Analytic function name
The function name is made up of letters, so we just take it character by character, determine if it’s a letter, if it is, record the character, and then recursively do the same for the rest of the string, until the character is not a letter, and in this way we can get the function name.
Let’s first define the type of the letter:
type alphaChars = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'
| 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';
Copy the code
There are also types to save intermediate results:
type TempParseResult<Token extends string, Rest extends string> = {
token: Token,
rest: Rest
}
Copy the code
Then it takes characters one by one to judge, and constructs the characters as strings and stores them in the intermediate result:
type parseFunctionName<SourceStr extends string, Res extends string = ' '>
= SourceStr extends `${infer PrefixChar}${infer RestStr}`
? PrefixChar extends alphaChars
? parseFunctionName<RestStr, `${Res}${PrefixChar}`>
: TempParseResult<Res, SourceStr>
: never;
Copy the code
We take a single character, determine if it’s a letter, construct a new string from that character, and recursively retrieve the rest of the string.
Test it out:
In accordance with our requirements, we resolve the function name by pattern matching substring.
And then go ahead and parse the rest.
Parsing the brackets
Parentheses match in the same way, and parentheses are only one character, do not need to recursively fetch, fetch once on the line.
type brackets = '(' | ') ';
type parseBrackets<SourceStr>
= SourceStr extends `${infer PrefixChar}${infer RestStr}`
? PrefixChar extends brackets
? TempParseResult<PrefixChar, RestStr>
: never
: never;
Copy the code
Test it out:
Continue parsing the rest:
Analytical figures
Number parsing is also a character by character, judge whether a match, if the match recursively take the next character, until no match:
type numChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type parseNum<SourceStr extends string, Res extends string = ' '>
= SourceStr extends `${infer PrefixChar}${infer RestStr}`
? PrefixChar extends numChars
? parseNum<RestStr, `${Res}${PrefixChar}`>
: TempParseResult<Res, SourceStr>
: never;
Copy the code
Test it out:
Continue parsing the rest:
Parsing the comma
A comma, like a parenthesis, is determined by a single character. No recursion is required.
type parseComma<SourceStr extends string>
= SourceStr extends `${infer PrefixChar}${infer RestStr}`
? PrefixChar extends ', '
? TempParseResult<', ', RestStr>
: never
: never;
Copy the code
Test it out:
At this point, we’ve parsed all of the characters, just to organize them in order.
The overall analytical
The whole parsing is to organize the order. After each parsing, we get the remaining strings and pass them to the next parsing logic. After all parsing, we can get all kinds of information.
type parse<SourceStr extends string, Res extends string = ' '>
= parseFunctionName<SourceStr, Res> extends TempParseResult<infer FunctionName, infer Rest1>
? parseBrackets<Rest1> extends TempParseResult<infer BracketChar, infer Rest2>
? parseNum<Rest2> extends TempParseResult<infer Num1, infer Rest3>
? parseComma<Rest3> extends TempParseResult<infer CommaChar, infer Rest4>
? parseNum<Rest4> extends TempParseResult<infer Num2, infer Rest5>
? parseBrackets<Rest5> extends TempParseResult<infer BracketChar2, infer Rest6>
? {
functionName: FunctionName,
params: [Num1, Num2],
}: never: never: never: never : never : never;
Copy the code
Test it out:
We’re done, and we’ve implemented a simple Parser with ts type!
Summary: TS type can take out substrings through pattern matching. We divide tokens recursively by taking and judging characters one by one, and then split tokens in order to achieve string parsing.
The complete code is as follows:
type numChars = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type alphaChars = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
| 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M'
| 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z';
type TempParseResult<Token extends string, Rest extends string> = {
token: Token,
rest: Rest
}
type parseFunctionName<SourceStr extends string, Res extends string = ' '> =
SourceStr extends `${infer PrefixChar}${infer RestStr}`
? PrefixChar extends alphaChars
? parseFunctionName<RestStr, `${Res}${PrefixChar}`>
: TempParseResult<Res, SourceStr>
: never;
type brackets = '(' | ') ';
type parseBrackets<SourceStr>
= SourceStr extends `${infer PrefixChar}${infer RestStr}`
? PrefixChar extends brackets
? TempParseResult<PrefixChar, RestStr>
: never
: never;
type parseNum<SourceStr extends string, Res extends string = ' '>
= SourceStr extends `${infer PrefixChar}${infer RestStr}`
? PrefixChar extends numChars
? parseNum<RestStr, `${Res}${PrefixChar}`>
: TempParseResult<Res, SourceStr>
: never;
type parseComma<SourceStr extends string>
= SourceStr extends `${infer PrefixChar}${infer RestStr}`
? PrefixChar extends ', '
? TempParseResult<', ', RestStr>
: never
: never;
type parse<SourceStr extends string, Res extends string = ' '>
= parseFunctionName<SourceStr, Res> extends TempParseResult<infer FunctionName, infer Rest1>
? parseBrackets<Rest1> extends TempParseResult<infer BracketChar, infer Rest2>
? parseNum<Rest2> extends TempParseResult<infer Num1, infer Rest3>
? parseComma<Rest3> extends TempParseResult<infer CommaChar, infer Rest4>
? parseNum<Rest4> extends TempParseResult<infer Num2, infer Rest5>
? parseBrackets<Rest5> extends TempParseResult<infer BracketChar2, infer Rest6>
? {
functionName: FunctionName,
params: [Num1, Num2],
}: never: never: never: never : never : never;
type res = parse<'add (11)'>;
Copy the code
Object class gymnastics
Exercise 4: Implement advanced type, take out the numeric attribute value in the object type
Construct an object, fetch the name of an attribute, and fetch the value of an attribute.
type filterNumberProp<T extends Object> = {
[Key in keyof T] : T[Key] extends number ? T[Key] : never
}[keyof T];
Copy the code
We construct a new object type, iterate over the property name of the object through keyof, evaluate the value of the property, return never if it is not a number, and then take the value of the property.
Attribute value returns never to indicate that the attribute does not exist, so the filtering effect can be achieved.
Test it out:
Summary: Object types can construct new objects by {}, take attribute values by [], and iterate through attribute names by keyof.
conclusion
TypeScript extends JavaScript’s syntax for types and supports advanced types to generate types.
Advanced types are types declared by type with type parameters, also called generics. The type calculation logic that generates final types from type parameters is nicknamed type gymnastics.
TypeScript’s type system is Turing-complete and can describe any computable logic:
- There are? : Can be used to make conditional judgments and is often used with extends
- Loops can be achieved through recursion
- {}, select keyof, select T[Key]
- You can do string construction
${a}${b}
The string pattern matches to take the substring STR extends${infer x}${infer y}
We did these types of exercises separately:
- Ts implement addition: recursively construct the array and then take the length
- Ts implements repeated strings: recursively construct arrays to count, then recursively construct strings
- Ts implements parser: Parses each part by matching string patterns and fetching substrings, and finally combines calls
- Ts object attribute filtering: by constructing the object, taking the attribute name, value syntax combination call
It should be noted that the number class should be calculated by constructing the length of array, and the pattern matching and infer of string should be saved to fetch substrings. These two are relatively difficult.
In fact, all kinds of advanced types, as long as you are familiar with the TS type grammar, think clearly logic can be written step by step, and write JS logic has no essential difference, but it is used to generate type logic.
Read here, is not the feeling of advanced type of type gymnastics is not what difficulty?