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, thenTSThe 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.

  • isassertions
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
  • inassertions
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:

  • objType problem.
  • keyType of problem.
  • getValueFunction 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 toTThe value of the second parameter of the function can only beobjObject.
  • T[U]: This code means fetchTType The definition type of the actual key name is the return value corresponding to the actual function return value: when the second argument is passednameWhen,getValue()The return value of the function isstring; When the second argument is passedageWhen,getValue()The return value of the function isnumber.
  • Type inference: although we givegetValue()Method defines two genericsTandU, but we don’t write generics in the actual function call,TSGenerics 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 : neverBecause:TIs a union type, so this applies toDistributed condition typeThe concept of. According to its concept, in the actual process will putTEach 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:TIs it a function type?
  • (... args: any) => infer RThis code actually says: a function whose arguments we useargsI’m going to use its return typeRTo place the space.
  • ifTSatisfy is a function type, so we return its function return type, i.eRIf 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:TIs it aPromisePackage type.
  • Promise<infer R>This code actually says:PromiseInside the actual package type, we useRTo hold a place, for example:
Promise<string> => R = string
Promise<number> => R = number 
Copy the code
  • ifTContentment is aPromisePackage type, then returns the type of its packageROtherwise 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 TThis code is fetchTAll keys in a type, all keys combined into a union type, for example:'name'|'age'.
  • P in keyof T:P inBelongs to an iterative process, can be usedJavaScriptIn thefor inIterate 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 operationTypeScriptMedium, cannot passT.PShould 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 iskeyofandin, which we have described in detail in the above example and will not repeat here.
  • -readonly: This code represents thereadonlyRemove 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 representsKiskeyof anySubtypes 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 TSaid:KCan only bekeyof TSubtype if we are usingPickWhen passed out does not exist inTWill 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:

  • useMyExclude<keyof T, K>, we can learn fromTTo get a union type, for example:'name'|'age'
  • useMyPick<T, 'name'|'age'>, we can start fromTTo 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: Willkey/valueConstructed askey/keyIn 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: UnderstandTSType 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: Willkey/valueConstructed askey/keyIn 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: UnderstandTSType 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:TThe type must be an array type.
  • T extends [infer L, ...infer R]:TIs it a case ofLIs the first element, and the remaining elements areRPlaceholder in the form of an array, whereRIt 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]:TIs it a case ofRIs the last element, and the remaining elements areLPlaceholder in the form of an array, whereLIt 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:TMust be an array type,USame thing.
  • [...T, ...U]: thisTandUThe 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 TIn 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'.S1Meet 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,StringToNumberThe 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 getdelimiterandparts, 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 handleJoinThe return value of the function, let’s define oneJoinTypeTo 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, improveJoinTypeUtility 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-likeshiftRemoves elements from the array header using the idea of pre-array matching.
  • P extends []: Here judgePWhether it is an empty array overrides case one.
  • P extends [infer Head]: Here judgepIs 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 putPThe elements in theDLink 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:ChainableThe first thing to satisfy is that the object existsoptionandgetThese two methods.
type Chainable<T> = {
  option(key: any.value:any) :any
  get(): any
}
Copy the code
  • Step 2: ProcessgetThe return type of the function
type Chainable<T> = {
  option(key: any.value:any) :any
  get(): T
}
Copy the code
  • Step 3: ProcessoptionThe 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…