TypeScript
Tools and Versions
Versions and Tools
- Typescript: 4.3.2
- Ts – node: 10.0.0
# installation typescript$NPM install -g [email protected]# ts - node installation$NPM install -g [email protected]Copy the code
knowledge
The joint type
A variety of values of a, the joint type said | is used to separate:
let value: string | number
value = 123 // Compile correctly
value = '456' // Compile correctly
value = true // Compile error
Copy the code
- When the union type is uncertain, only its common properties or methods can be taken.
// It will compile an error: it is not certain whether the type is string or number
function getLength (value: string | number) :number {
return value.length
}
String and number both have toString() methods
function getString (value: string | number) :string {
return value.toString()
}
Copy the code
- When a variable has its type determined, then
TS
The corresponding property or method is automatically derived.
let value: string | number
value = '123'
console.log(value.length) // Compile correctly
value = 123
console.log(value.length) // Compile error
Copy the code
String literals
A string literal is very similar to a union type in that it indicates that a variable can take only one of several characters.
type EventName = 'click' | 'scroll' | 'mousemove'
function handleEvent (event: EventName) {
console.log(event)
}
handleEvent('click') // Compile correctly
handleEvent('scroll') // Compile correctly
handleEvent('dbclick') // Error compiling
Copy the code
tuples
An array with a definite length and a definite element type for each position is called a tuple.
According to the above definition, we summarize the following two characteristics of tuples:
- Array lengths do not match, an error is reported.
let arr: [string.number]
arr = ['123'.456.789] // Compile error
arr = ['123'] // Compile error
arr = ['123'.456] // Compile correctly
Copy the code
- Element types do not match, an error is reported.
let arr: [string.number]
arr = [123.'456'] // Compile error
arr = ['123'.456] // Compile correctly
Copy the code
The enumeration
Enumeration types are used to indicate that values are limited to a specified range, such as seven days in a week, red, green, blue, and so on.
enum Colors {
Red = 1,
Yellow = 2,
Blue = 3
}
// Positive value
console.log(Colors.Red) / / 1
console.log(Colors.Yellow) / / 2
console.log(Colors.Blue) / / 3
// Reverse the value
console.log(Colors[1]) // Red
console.log(Colors[2]) // Yellow
console.log(Colors[3]) // Blue
Copy the code
Function overloading
JavaScript, because of its dynamic language nature, doesn’t really have the concept of function overloading. In TypeScript, function overloading is just multiple declarations of the same function (with different numbers of arguments or different types of arguments) that start matching from the first function declaration when the function starts matching.
function getArea (width: number) :number
function getArea (width: number, height? :number) :number
function getArea (width: number, height? :number) :number {
if (height) {
return width * height
} else {
return width * width
}
}
console.log(getArea(10.20)) / / 200
console.log(getArea(10)) / / 100
Copy the code
Type guard (type assertion)
When a variable is of a combined type, TypeScript does some extra work to figure out what type it is. The most common types of assertion are IS and in.
is
assertions
function isString (str: any) :str is string {
return typeof str === 'string'
}
function getLength (value: string | number) :number {
if (isString(value)) {
return value.length
}
return value.toString().length
}
console.log(getLength('123')) / / 3
console.log(getLength(123)) / / 3
Copy the code
in
assertions
class Person {
sayHi () {
console.log('Hello~')}}class Animal {
bark () {
console.log(Woof woof woof)}}function sound (obj: Person | Animal) :void {
if ('sayHi' in obj) {
obj.sayHi()
} else {
obj.bark()
}
}
sound(new Person()) // Hello~
sound(new Animal()) / / auf
Copy the code
The generic
Learn about generics from an example.
function getValue (obj, key) {
return obj[key]
}
Copy the code
To add TypeScript support to the above methods, we need to address the following three issues:
obj
Type problem.key
Type of problem.getValue
Function return value type problem.
Let’s transform the above method preliminarily:
function getValue (obj: any, key: string) :any {
return obj[key]
}
Copy the code
We found that without using generics, the above approach had very limited type support. Next, we’ll continue the transformation with generics.
function getValue<T> (obj: T, key: keyof T) :T[keyof T] {
return obj[key]
}
Copy the code
After the transformation, we found that a new keyword keyof was introduced. Where keyof T represents the union type of keys of type T.
const obj = {
name: 'AAA'.age: 23
}
// 'name'|'age'
type keys = keyof typeof obj
Copy the code
Keyof T in TS is equivalent to object.keys () in JavaScript.
After introducing keyof, we continue to refine the getValue method as follows:
function getValue<T.U extends keyof T> (obj: T, key: U) :T[U] {
return obj[key]
}
const obj = {
name: 'AAA'.age: 23
}
console.log(getValue(obj, 'name')) // Compilation succeeded
console.log(getValue(obj, 'age')) // Compilation succeeded
console.log(getValue(obj, 'sex')) // Failed to compile, 'sex' property does not exist
Copy the code
Code details:
U extends keyof T
: this code indicates that the value of the second generic is limited toT
The value of the second parameter of the function can only beobj
Object.T[U]
: This code means fetchT
Type The definition type of the actual key name is the return value corresponding to the actual function return value: when the second argument is passedname
When,getValue()
The return value of the function isstring
; When the second argument is passedage
When,getValue()
The return value of the function isnumber
.- Type inference: although we give
getValue()
Method defines two genericsT
andU
, but we don’t write generics in the actual function call,TS
Generics are automatically derived from the actual arguments.
// Automatically derive
getValue<{name: string; age:number},'name'>(obj, 'name')
getValue<{name: string; age:number},'age'>(obj, 'age')
Copy the code
extends
In the introduction to generics section, we introduced the extends keyword. There are generally two ways to use the extends keyword: type constraints and conditional types.
Type constraints
Type constraints are often used with generics. Take an example from the generics section:
// Type constraint
U extends keyof T
Copy the code
Keyof T is a whole that represents a union type. The U extends Union section represents that the type of U is contracted to a Union type.
This is what happens: the string passed by the second argument can only be one of the T keys. Passing a nonexistent key is an error.
Conditions in the
Common condition types are as follows:
T extends U ? 'Y' : 'N'
Copy the code
We found that conditional types are a bit like JavaScript ternary expressions, in fact they work similarly, for example:
type res1 = true extends boolean ? true : false // true
type res2 = 'name' extends 'name'|'age' ? true : false // true
type res3 = [1.2.3] extends { length: number; }?true : false // true
type res4 = [1.2.3] extends Array<number>?true : false // true
Copy the code
Among the condition types, one particular thing to note is the distributed condition type, as follows:
// Built-in tools: intersection
type Extract<T, U> = T extends U ? T : never;
type type1 = 'name'|'age'
type type2 = 'name'|'address'|'sex'
// Result: 'name'
type test = Extract<type1, type2>
// Reasoning steps
'name'|'age' extends 'name'|'address'|'sex' ? 'name'|'age' : never= > ('name' extends 'name'|'address'|'sex' ? 'name' : never) |
('age' extends 'name'|'address'|'sex' ? 'age' : never) = > 'name' | never= > 'name'
Copy the code
Code details:
T extends U ? T : never
Because:T
Is a union type, so this applies toDistributed condition typeThe concept of. According to its concept, in the actual process will putT
Each subtype of a type iterates as follows:
// First iteration:
'name' extends 'name'|'address'|'sex' ? 'name' : never
// Second iteration:
'age' extends 'name'|'address'|'sex' ? 'age' : never
Copy the code
- After the iteration is complete, the results of each iteration are combined into a new union type (culling)
never
), as follows:
type result = 'name' | never= > 'name'
Copy the code
Infer the keywords
introduce
Infer keyword is used to delay the derivation. It will carry out placeholder when the type is not deduced and accurately return the correct type after the derivation is successful.
To better understand the use of the infer keyword, we use ReturnType and PromiseType.
ReturnType
ReturnType is a tool for getting the ReturnType of a function.
type ReturnType<T> = T extends(... args:any) => infer R ? R : never
type UserInfo = {
name: string;
age: number;
}
function login(username: string, password: string) :UserInfo {
const userInfo = { name: 'admin'.age: 99 }
return userInfo
}
// Result: {name: string; age: number; }
type result = MyReturnType<typeof login>
Copy the code
Code details:
T extends (... args: any) => infer R
: If you don’t lookinfer R
, this code actually says:T
Is it a function type?(... args: any) => infer R
This code actually says: a function whose arguments we useargs
I’m going to use its return typeR
To place the space.- if
T
Satisfy is a function type, so we return its function return type, i.eR
If it is not a function type, we returnnever
.
PromiseType
PromiseType is a tool used to get the type of Promise package.
type MyPromiseType<T> = T extends Promise<infer R> ? R : never
// Result: string[]
type result = MyPromiseType<Promise<string[] > >Copy the code
Code details:
T extends Promise<infer R>
This code actually says:T
Is it aPromise
Package type.Promise<infer R>
This code actually says:Promise
Inside the actual package type, we useR
To hold a place, for example:
Promise<string> => R = string
Promise<number> => R = number
Copy the code
- if
T
Contentment is aPromise
Package type, then returns the type of its packageR
Otherwise returnnever
.
Challenge question
For the basics, we’ll refer to other TypeScript articles and the TypeScript website.
In the part of Challenges, we prefer to use the above knowledge to solve practical problems. This part mainly refers to type-challenges, which are divided into the following types:
- Built-in utility classes
- Built-in utility class extensions
- An array of class
- String class
- Pass to categorize
- Actual scenario questions
For each category, there may be three types of difficulty: simple, general and difficult.
Built-in utility classes
Built-in utility classes are the utility functions that TypeScript officially provides by default. You can find their native implementations in lib.es5.d.ts.
Required or Partial
Whereas Required is used to make all fields mandatory, Partial does the opposite, which is used to make all fields fillable.
type MyPartial<T> = {
[P inkeyof T]? : T[P] }type MyRequired<T> = {
[P inkeyof T]-? : T[P] }type Person = {
name: string; age? :number;
}
// Result: {name: string; age: number; }
type result1 = MyRequired<Person>
// Result: {name? : string; age? : number; }
type result2 = MyPartial<Person>
Copy the code
Code details:
keyof T
This code is fetchT
All keys in a type, all keys combined into a union type, for example:'name'|'age'
.P in keyof T
:P in
Belongs to an iterative process, can be usedJavaScript
In thefor in
Iterate to understand, for example:
P in 'name'|'age'
// First iteration: P = 'name'
// Second iteration: P = 'age'
Copy the code
T[P]
: is a normal value operationTypeScript
Medium, cannot passT.P
Should be usedT[P]
.-?
This code can be removed from the database?
This notation.
Readonly and Mutalbe
Readonly, which is used to make all fields Readonly, does the opposite; it is used to make all fields writable.
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
type MyMutable<T> = {
-readonly [P in keyof T]: T[P]
}
type Person = {
name: string; readonly age? : number; }// Result: {readonly name: string; readonly age? : number; }
type result1 = MyReadonly<Person>
// Result: {name: string; age? : number; }
type result2 = MyMutable<Person>
Copy the code
Code details:
- Student: Here it is
keyof
andin
, which we have described in detail in the above example and will not repeat here. -readonly
: This code represents thereadonly
Remove the keyword, and when you remove it, you go from read-only to writable.
Record (structure)
Record acts a bit like the map method in JavaScript. It is used to assign each key (K) of K to type T, so that multiple K/TS are combined into a new type.
type MyRecord<k extends keyof any, T> = {
[P in K]: T
}
type pageList = 'login' | 'home'
type PageInfo = {
title: string;
url: string;
}
type Expected = {
login: { title: string; url: string; };
home: { title: string; url: string; };
}
// Result: Expected
type result = MyRecord<pageList, PageInfo>
Copy the code
Code details:
k extends keyof any
: This code representsK
iskeyof any
Subtypes of all keys of any type, for example:
/ / K as' Dog '|' cat '
type UnionKeys = 'Dog' | 'Cat'
/ / K for 'name' | 'age'
type Person = {
name: string;
age: number;
}
type TypeKeys = keyof Person
Copy the code
Pick (selection)
Pick selects the specified fields from the specified type to form a new type.
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type Person = {
name: string;
age: number;
address: string;
}
// Result: {age: number; address: string; }
type result = MyPick<Person, 'age' | 'address'>
Copy the code
K extends keyof T
Said:K
Can only bekeyof T
Subtype if we are usingPick
When passed out does not exist inT
Will return an error:
// Error: Phone cannot be assigned to keyof T
type result = MyPick<Person, 'name' | 'phone'>
Copy the code
Exclude (out)
Exclude removes the types that exist in U from T, i.e. the difference set.
type MyExclude<T, U> = T extends U ? never : T
// Result: 'name'
type result = MyExclude<'name'|'age'|'sex'.'age'|'sex'>
Copy the code
Explanation of code: We looked at a similar problem earlier in the extends section, except that we looked at the intersection, in this case the difference set. But the principle is similar, it’s a distributed condition type of concept.
T extends U ? never ? T
// First iteration distribution:
'name' extends 'age' | 'sex' ? never : 'name'= >'name'
// The second iteration is distributed:
'age' extends 'age' | 'sex' ? never : 'age'= >never
// The third iteration is distributed:
'sex' extends 'age' | 'sex' ? never : 'sex'= >never
/ / the result:
type result = 'name' | never | never= > 'name'
Copy the code
Omit (rejecting)
What Omit does, as opposed to Pick, is to remove the specified fields from the specified T type, leaving the fields to form a new type.
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
type MyExclude<T, U> = T extends U ? never : T
type MyOmit<T, K> = MyPick<T, MyExclude<keyof T, K>>
type Person = {
name: string;
age: number;
address: string;
}
// Result: {name: string; age:number; }
type result = MyOmit<Person, 'address'>
Copy the code
Code details:
- use
MyExclude<keyof T, K>
, we can learn fromT
To get a union type, for example:'name'|'age'
- use
MyPick<T, 'name'|'age'>
, we can start fromT
To combine the two fields into a new type.
Built-in utility class extensions
In the built-in Utility Class Extension section, we extend the RequiredKeys, OptionalKeys, GetRequired, and GetOptional utility methods together with the Required and Readonly built-in tools previously.
RequiredKeys(All required fields)
RequiredKeys is used to retrieve all required fields that are combined into a union type.
type RequiredKeys<T> = {
[P in keyof T]: T extends Record<P,T[P]> ? P : never
}[keyof T]
type Person = {
name: string; age? :number; address? :string;
}
// Result: 'name'
type result = RequiredKeys<Person>
Copy the code
RequiredKeys implementation:
- Step 1: Will
key/value
Constructed askey/key
In the form of:
type RequiredKeys<T> = {
[P in keyof T]: P
}
type Person = {
name: 'name'; age? :'age'; address? :'address';
}
Copy the code
- The second step:
T[keyof T]
The value yields a union type:
type RequiredKeys<T> = {
[P in keyof T]: P
}[keyof T]
type Person = {
name: 'name'; age? :'age'; address? :'address';
}
// 'name'|'age'|'address'
type keys = RequiredKeys<Person>
Copy the code
- Step 3: Understand
TS
Type relation, type specific is a subclass, type broad is the parent class.
// Result: true
type result1 = Person extends { name: string; }?true : false
// Result: false
type result2 = Person extends{ age? :number; }?true : false
Copy the code
Based on the above knowledge, we can use the following line of code to represent this relationship:
T extends Record<P, T[P]> ? P : never
Copy the code
Substituting the above example, the result is:
Person extends Record<'name'.string>?'name' : never // 'name'
Person extends Record<'age'.number>?'age' : never // never
Copy the code
- Step 4: Complete implementation
type RequiredKeys<T> = {
[P in keyof T]: T extends Record<P,T[P]> ? P : never
}[keyof T]
Copy the code
OptionalKeys(all optional fields)
type OptionalKeys<T> = {
[P in keyof T]: {} extends Pick<T, P> ? P : never
}[keyof T]
type Person = {
name: string; age? :number; address? :string;
}
/ / the result: 'age' | 'address'
type result = OptionalKeys<Person>
Copy the code
Implementation idea:
- Step 1: Will
key/value
Constructed askey/key
In the form of:
type OptionalKeys<T> = {
[P in keyof T]: P
}
type Person = {
name: 'name'; age? :'age'; address? :'address';
}
Copy the code
- The second step:
T[keyof T]
The value yields a union type:
type OptionalKeys<T> = {
[P in keyof T]: P
}[keyof T]
type Person = {
name: 'name'; age? :'age'; address? :'address';
}
// 'name'|'age'|'address'
type keys = OptionalKeys<Person>
Copy the code
- Step 3: Understand
TS
Type relation, type specific is a subclass, type broad is the parent class.
// Result: true
type result = {} extends{ age? :string; }?true : false
Copy the code
You can extend {} extends {age? : string; } this is true.
type result = {} extends {} | {age: string;} ? true : false
Copy the code
Based on the above example, we use a line of code to represent this relationship:
{} extends Pick<T, P> ? P : never
Copy the code
For our example, the result is as follows:
{} extends Pick<Person, 'name'>?'name' : never= > 'name'
{} extends Pick<Person, 'age'>?'age' : never= > never
{} extends Pick<Person, 'address'>?'address' : never= > never
Copy the code
- Complete implementation
type OptionalKeys<T> = {
[P in keyof T]: {} extends Pick<T, P> ? P : never
}[keyof T]
Copy the code
GetRequired(All required types)
GetRequired is used for obtaining a new type consisting of all the required keys and their types in a type.
With RequiredKeys implemented, we can easily implement GetRequired
type GetRequired<T> = {
[P inRequiredKeys<T>]-? : T[P] }type Person = {
name: string; age? :number; address? :string;
}
// Result: {name: string; }
type result = GetRequired<Person>
Copy the code
GetOptional(All types optional)
GetOptional is used to get a new type of all the optional keys and their types in a type.
After implementing OptionalKeys, it was easy to implement GetOptional
type GetOptional<T> = {
[P inOptionalKeys<T>]? : T[P] }type Person = {
name: string; age? :number; address? :string;
}
// result: {age? : number; address? : string; }
type result = GetOptional<Person>
Copy the code
An array of class
The infer keyword is often used to match arrays. It can be used in the following common ways:
// Matches an empty array
T extends[]?true : false
// Array pre-match
T extends [infer L, ...infer R] ? L : never
// The array is matched
T extends [...infer L, infer R] ? R: never;
Copy the code
FirstOfArray(first element of array)
Using the idea of pre-array matching, we can easily implement the tool to get the first element of an array, FirstOfArray.
type FirstOfArray<T extends any[]> = T extends [infer L, ...infer R] ? L : never
// Result: 1
type result = FirstOfArray<[1.2.3] >Copy the code
Code details:
T extends any[]
Limit:T
The type must be an array type.T extends [infer L, ...infer R]
:T
Is it a case ofL
Is the first element, and the remaining elements areR
Placeholder in the form of an array, whereR
It can be an empty array. The following forms satisfy the above conditions.
// L = 1, R = [2, 3, 4]
const arr1 = [1.2.3.4]
// L = 1, R = []
const arr2 = [1]
Copy the code
LastOfArray(last element of array)
Using the idea of matching after an array, we can easily implement the tool to get the last element of the array, LastOfArray.
type LastOfArray<T extends any[]> = T extends [...infer L, infer R] ? R : never
// Result: 3
type result = Last<[1.2.3] >Copy the code
Code details:
T extends [...infer L, infer R]
:T
Is it a case ofR
Is the last element, and the remaining elements areL
Placeholder in the form of an array, whereL
It can be an empty array. The following forms satisfy the above conditions.
// L = [1, 2, 3], R = 4
const arr1 = [1.2.3.4]
// L = [] R = 1
const arr2 = [1]
Copy the code
ArrayLength(ArrayLength)
To get the length of the array, use T[‘length’] directly.
Note: the value cannot be in the form of t. length.
type ArrayLength<T extends readonly any[]> = T['length']
// Result: 3
type result = ArrayLength<[1.2.3] >Copy the code
Extension: In the above implementation, we can only pass a generic array. If we want to be compatible with passing class arrays, we need to change the code as follows:
type ArrayLength<T> = T extends { length: number}? T['length'] : never
type result1 = ArrayLength<[1.2.3] >/ / 3
type result2 = ArrayLength<{ 0: '0'.length: 12} >/ / 12
Copy the code
Concat(array Concat method)
According to the use of array concat, we just need to take two arrays and use the expansion operator… Expand into a new array.
type MyConcat<T extends any[], U extends any[]> = [...T, ...U]
// Result: [1, 2, 3, 4]
type result = MyConcat<[1.2], [3.4] >Copy the code
Code details:
T extends any[]
Limit:T
Must be an array type,U
Same thing.[...T, ...U]
: thisT
andU
The representatives represent the array type, so you can use the expansion operator to expand the array.
Includes(array Includes method)
In TS, all elements in an array can be represented by T[number], which is an associative type composed of all elements.
type MyIncludes<T extends any[], K> = K extends T[number]?true : false
type result1 = MyIncludes<[1.2.3.4].'4'> // false
type result2 = MyIncludes<[1.2.3.4].4> // true
Copy the code
Code details:
T[number]
: represents a union type common to all elements of an array. For example:1 | 2 | 3 | 4
.
Push and Pop(array Push and Pop methods)
The Push method is easy to implement. For the Pop method, we need to use the idea of matching after an array.
type MyPush<T extends any[], K> = [...T, K]
type MyPop<T extends any[]> = T extends [...infer L, infer R] ? L : never
type result1 = MyPush<[1.2.3].4> // [1, 2, 3, 4]
type result2 = MyPush<[1.2.3], [4.5] >// [1, 2, 3, [4, 5]]
type result3 = MyPop<[1.2.3] >/ / [1, 2]
type result4 = MyPop<[1] >/ / []
Copy the code
String class
The infer keyword is used to match strings and arrays, but its expression is different. For example:
S extends `${infer S1}${infer S2}` ? S1 : never
T extends [infer L, ...infer R] ? L : never
Copy the code
StringLength(StringLength)
Implementation idea: Use recursion, infer placeholder and auxiliary array ideas to achieve.
type StringLength<S extends string, T extends any[] = []> =
S extends `${infer S1}${infer S2}`
? LengthOfString<S2, [...T, S1]>
: T['length']
type result1 = StringLength<' '> / / 0
type result2 = StringLength<'123'> / / 3
type result3 = StringLength<' 1 2 3 '> / / 7
Copy the code
Let’s take the second example above as an example to elaborate:
type result2 = StringLength<'123'> / / 3
S extends' ${infer S1}${infer S2}
S = '123' S1 = '1' S2 = '23' T = []
S extends' ${infer S1}${infer S2}
S = '23' S1 = '2' S2 = '3' T = ['1']
S extends' ${infer S1}${infer S2}
S = '3' S1 = '3' S2 = ' ' T = ['1'.'2']
S extends' ${infer S1}${infer S2}
S = ' ' S1 = ' ' S2 = ' ' T = ['1'.'2'.'3']
// Result: 3
type result = T['length']
Copy the code
Capitalize(Capitalize)
Capitalize is used to Capitalize the first letter of a string, using the built-in Uppercase tool.
type MyCapitalize<S extends string> =
S extends `${infer S1}${infer S2}`
? `${Uppercase<S1>}${S2}`
: S
// result: Abc
type result = MyCapitalize<'abc'>
Copy the code
${infer S1}${infer S2} = ‘BC’ and S1 = ‘a’ and S2 = ‘BC’.
Extension: With this in mind, we can write a MyUnCapitalize tool that does the opposite of MyUnCapitalize and implements the following code:
type MyUnCapitalize<S extends string> =
S extends `${infer S1}${infer S2}`
? `${Lowercase<S1>}${S2}`
: S
Copy the code
StringToArray
StringToArray is an easy tool to implement using the StringLength approach.
type StringToArray<S extends string, T extends any[] = []> =
S extends `${infer S1}${infer S2}`
? StringToArray<S2, [...T, S1]>
: T
// result: ['a', 'b', 'c']
type result = StringToArray<'abc'>
Copy the code
StringToUnion
Using recursion, we can also quickly implement StringToUnion, which converts a string to a union type.
type StringToUnion<S extends string> =
S extends `${infer S1}${infer S2}`
? S1 | StringToUnion<S2>
: never
/ / result: 'a' | 'b' | 'c'
type result = StringToUnion<'abc'>
Copy the code
CamelCase(hyphen to small hump)
CamelCase is a tool for converting hyphenated strings into small humps.
type CamelCase<S extends string> =
S extends `${infer S1}-${infer S2}`
? S2 extends Capitalize<S2>
? `${S1}-${CamelCase<S2>}`
: `${S1}${CamelCase<Capitalize<S2>>}`
: S
// Result: 'fooBarBaz'
type result = CamelCase<'foo-bar-baz'>
Copy the code
Code description: CamelCase implementation, the same use of recursive thinking, we take the above example as an example for detailed explanation:
type result = CamelCase<'foo-bar-baz'>
${infer S1}-${infer S2} S2 does not satisfy the extends Capitalize
S = 'foo-bar-baz' S1 = 'foo' S2 = 'bar-baz'
${infer S1}-${infer S2} S2 does not satisfy the extends Capitalize
S = 'Bar-baz' S1 = 'Bar' S2 = 'baz'
${infer S1}-${infer S2}
S = 'Baz'
// Result: fooBarBaz
type result = 'foo' + 'Bar' + 'Baz'= >'fooBarBaz'
Copy the code
Get(attribute path value)
You’ve probably heard of, or used, the LoDash library’s get method, which allows an object to be evaluated as a string path, such as:
import _ from 'lodash'
const obj = {
foo: {
bar: {
value: 'foobar'.count: 6,},included: true,},hello: 'world'
}
console.log(_.get(obj, 'foo.bar.value')) // 'foobar'
Copy the code
We can do the same thing in TS, the Get tool.
type Get<T, K extends string> =
K extends `${infer S1}.${infer S2}`
? S1 extends keyof T
? Get<T[S1], S2>
: never
: K extends keyof T
? T[K]
: never
type Data = {
foo: {
bar: {
value: 'foobar'.count: 6,},included: true,},hello: 'world'
}
type result1 = Get<Data, 'hello'> // 'world'
type result2 = Get<Data, 'foo.bar.value'> // 'foobar'
type result3 = Get<Data, 'baz'> // never
Copy the code
Code details:
- For the first example, it doesn’t
${infer S1}.${infer S2}
Form, but satisfykeyof T
In this case, we just need to simply value according to the key name, so the result is'world'
- For the second example, it satisfies
${infer S1}.${infer S2}
At this timeS1='foo'
.S1
Meet againkeyof T
, so there is a recursive call, we use the following code to detail the recursive process.
${infer S1}.${infer S2} and S1 satisfy keyof T
T = Data K = 'foo.bar.value' S1 = 'foo' S2 = 'bar.value'
${infer S1}.${infer S2}, S1, keyof T
T = Data['foo'] K = 'bar.value' S1 = 'bar' S2 = 'value'
${infer S1}.${infer S2}, K, keyof T
T = Data['foo'] ['bar'] K = 'value'
// Result: 'foobar'
type result = Data['foo'] ['bar'] ['value']
Copy the code
StringToNumber(to numbers)
StringToNumber is a tool that converts a string number into a real number number.
In JavaScript, we can easily call the Number() method or the parseInt() method to convert a string Number to a numeric Number. In TS, however, there is no such method, which we need to implement manually.
// Result: 123
type result = StringToNumber<'123'>
Copy the code
The implementation of StringToNumber is not easy to understand, so we need to break it up and improve it step by step.
- Step 1: You can easily get strings
'123'
For each bit of character, we store it in an auxiliary arrayT
, as follows:
type StringToNumber<S extends string, T extends any[] = []> =
S extends `${infer S1}${infer S2}`
? StringToNumber<S2, [...T, S1]>
: T
// Result: ['1', '2', '3']
type result = StringToNumber<'123'>
Copy the code
- Step 2: We need to convert a single string number to a real number. We can use an intermediate array to help. For example:
'1'= > [0] ['length'] = >1
'2'= > [0.0] ['length'] = >2
'3'= > [0.0.0] ['length'] = 3.'9'= > [0.0.0.0.0.0.0.0.0] ['length'] = >9
Copy the code
According to the above rules, we wrap a MakeArray method, which is implemented as follows:
type MakeArray<N extends string, T extends any[] = []> =
N extends `${T['length']}`
? T
: MakeArray<N, [...T, 0] >type t1 = MakeArray<'1'> / / [0]
type t2 = MakeArray<'2'> / / [0, 0]
type t3 = MakeArray<'3'> / / [0, 0, 0]
Copy the code
- Step 3: Now that we have the hundreds, tens, and ones digits, we should use arithmetic to add them up in a certain pattern, as follows:
const arr = [1.2.3]
let target = 0
// First iteration
target = 10 * 0 + 1 = 1
// The second iteration
target = 10 * 1 + 2 = 12
// The third iteration
target = 10 * 12 + 3 = 123
Copy the code
According to the above ideas, we also need a Multiply10 tool function, corresponding to the actual demand, that is, to copy an array ten times, so we package a Multiply10 tool, the implementation code is as follows:
type Multiply10<T extends any[]> = [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
type result = Multiply10<[1] >// [1, 1, 1, 1, 1, 1, 1, 1]
Copy the code
- Step 4: Based on the analysis of the previous steps, string everything together,
StringToNumber
The complete implementation code is as follows:
type Digital = '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9'
type Multiply10<T extends any[]> = [...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T, ...T]
type MakeArray<N extends string, T extends any[] = []> =
N extends `${T['length']}`
? T
: MakeArray<N, [...T, 0] >type StringToNumber<S extends string, T extends any[] = []> =
S extends `${infer S1}${infer S2}`
? S1 extends Digital
? StringToNumber<S2, [...Multiply10<T>, ...MakeArray<S1>]>
: never
: T['length']
Copy the code
- Step 5: To better understand the recursive process, we break it down into the following steps:
type result = StringToNumber<'123'>
${infer S1}${infer S2} and S1 satisfy Digital
S = '123' S1 = '1' S2 = '23' T = [0] T['length'] = 1
${infer S1}${infer S2} and S1 satisfy Digital
S = '23' S1 = '2' S2 = '3' T = [0.. 0] T['length'] = 10
${infer S1}${infer S2} and S1 satisfy Digital
S = '3' S1 = '3' S2 = ' ' T = [0.. 0] T['length'] = 120
${infer S1}${infer S2} T['length'
S = ' ' T = [0.. 0] T['length'] = 123
/ / the result:
type result = StringToNumber<'123'> / / 123
Copy the code
Pass to categorize
Although we’ve already used recursion in the previous challenges, it’s worth mentioning. We select representative DeepReadonly and DeepPick to illustrate.
Readonly DeepReadonly (depth)
DeepReadonly is a tool for making all fields of a type read-only. For now, we only need to worry about nested objects.
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends { [key: string] :any}? DeepReadonly<T[P]> : T[P] }type Person = {
name: string;
age: number;
job: {
name: string;
salary: number; }}type Expected = {
readonly name: string;
readonly age: number;
readonly job: {
readonly name: string;
readonly salary: number; }}// Result: Expected
type result = DeepReadonly<Person>
Copy the code
In the implementation of DeepReadonly, we use T[P] extends {[key: string]: any} to determine if the current value is an object and is matched using index signatures. If satisfy is an object, then recursively call DeepReadonly.
P = 'name' T[P] is not an object
P = 'age' T[P] is not an object
P = 'job' [P] is an object
P = 'name' T[P] is not an object
P = 'salary' [P] is not an object
Copy the code
Pick DeepPick (depth)
DeepPick is a tool for depth estimation, as follows:
type Obj = {
a: number.b: string.c: boolean.obj: {
d: number.e: string.f: boolean.obj2: {
g: number.h: string.i: boolean,}},obj3: {
j: number.k: string.l: boolean,}}type Expected = {
obj: {
obj2: {
g: number}}}// Result: Expected
type result = DeepPick<Obj, 'obj.obj2.g'>
Copy the code
You may be familiar with this method. Yes, it is implemented in the same way as the Get property path, but with a minor change, the complete code is as follows:
// Compare with DeepPick
type Get<T, K extends string> =
K extends `${infer S1}.${infer S2}`
? S1 extends keyof T
? Get<T[S1], S2>
: never
: K extends keyof T
? T[K]
: never
type DeepPick<T, S extends string> =
S extends `${infer S1}.${infer S2}`
? S1 extends keyof T
? { [K in S1]: Get<T[S1], S2> }
: never
: S extends keyof T
? { [K in S]: T[K] }
: never
Copy the code
Scenario questions
The Join method
The Join method is somewhat similar to the Join method of array, but a little different. The specific test cases are as follows:
// Case 1: No arguments, return an empty string
join(The '-') ()/ /"
// Case 2: return only one argument
join(The '-') ('a') // 'a'
// Case 3: Multiple arguments, separated by delimiters
join(' ') ('a'.'b') // 'ab'
join(The '#') ('a'.'b') // 'a#b'
Copy the code
We need to implement a Join method that looks like this:
declare function join(delimiter: any) : (. parts:any[]) = >any
Copy the code
Implementation idea:
- Step 1: To be able to get
delimiter
andparts
, we introduce two generics.
declare function join<D extends string> (delimiter: D).P extends string[] > (. parts: P) = >any
Copy the code
- Step 2: In order to be able to handle
Join
The return value of the function, let’s define oneJoinType
To represent.
type JoinType<D extends string, P extends string[] > =any
declare function join<D extends string> (delimiter: D).P extends string[] > (. parts: P) = >JoinType<D.P>
Copy the code
- Step 3: Based on the case written before, improve
JoinType
Utility functions.
type Shift<T extends any[]> = T extends [infer L, ...infer R] ? R : []
type JoinType<D extends string, P extends string[]> =
P extends[]?' '
: P extends [infer Head]
? Head
: `${P[0]}${D}${JoinType<D, Shift<P>>}`
Copy the code
JoinType implementation:
Shift
: You need to implement an array-likeshift
Removes elements from the array header using the idea of pre-array matching.P extends []
: Here judgeP
Whether it is an empty array overrides case one.P extends [infer Head]
: Here judgep
Is it an array with only one element? Override case two.${P[0]}${D}${JoinType<D, Shift<P>>}
: The idea of recursion is used here, and the main process is to putP
The elements in theD
Link it up.
P = ['a', 'b'];
// In the first iteration, P is not an empty array and is a multi-element array.
P = ['a'.'b'] result = `a#`
// In the second iteration, P is not an empty array, but has only one element.
P = ['b'] result = `a#b`
/ / the result
type result = Join(The '#') ('a'.'b') // 'a#b'
Copy the code
Chainable call
In daily development, chained calls are often used. There is an object with two methods: Options and get. The option method is used to add new attributes, and the get method is used to get the latest object.
// Result: {foo: 123, bar: {value: 'Hello'}, name: 'TypeScript'}
const result = obj
.option('foo'.123)
.option('bar', { value: 'Hello' })
.option('name'.'TypeScript')
.get()
Copy the code
Implement a Chainable utility function to support this behavior of objects.
- The first step:
Chainable
The first thing to satisfy is that the object existsoption
andget
These two methods.
type Chainable<T> = {
option(key: any.value:any) :any
get(): any
}
Copy the code
- Step 2: Process
get
The return type of the function
type Chainable<T> = {
option(key: any.value:any) :any
get(): T
}
Copy the code
- Step 3: Process
option
The parameter type of a function and its return type.
type Chainable<T = {}> = {
options<K extends string, V>(key: K, value:V): Chainable<T & { [P in K]: V }>
get(): { [P in keyof T]: T[P] }
}
Copy the code
In this step, a new knowledge is introduced: cross types, which can be understood using the JavaScript object Merge.
Note: When two union types cross with &, it behaves a little differently, taking the intersection.
// Result: {name: string; age: number; address: string; }
type result1 = { name: string; age: number; } and {address: string; }
// Result: 'age'
type result2 = ('name' | 'age') & ('age' | 'address')
Copy the code
- Step 4: Test.
type Expected = {
foo: number
bar: {
value: string
}
name: string
}
declare const obj: Chainable<Expected>
// test
const result = obj
.options('foo'.123)
.options('bar', { value: 'Hello' })
.options('name'.'TypeScript')
.get()
Copy the code
reference
- Github.com/type-challe…
- Mp.weixin.qq.com/s/wQhBbnqzX…
- Juejin. Cn/post / 686591…