preface

A repository of TypeScript exercises was found on GitHub some time ago: There are 15 typescript-exercises in typescript-Exercises that cover elementary to advanced uses of TS. I’ve selected a few of them and let’s take a look at them and see what you can do right to consolidate your knowledge. Without further discussion, let’s get started:

exercises

I will write the GitHub address of the topic at the end of each topic. You can click on it to view the details of the topic. It is recommended that you clone the whole project and view it on your own computer.

exercises-7

Making address:exercises-7
The title
// index.ts
export function swap(v1, v2) {
  return [v2, v1]
}
Copy the code
// test.ts
import { swap } from './index'

const pair1 = swap(123.'hello') // Pair1 is of type any
typeAssert<IsTypeEqual<typeof pair1, [string.number] > > ()// This line will report an error because any and [string, number] are not equal
Copy the code
answer

Swap (123, ‘hello’) returns a result of type [string, number]. Swap (123, ‘hello’) returns a result of type [string, number]. Swap (123, ‘hello’) returns a result of type [string, number].

export function swap<T1.T2> (v1: T1, v2: T2) :T2.T1] {
  return [v2, v1]
}
Copy the code

Thus, TS can infer that swap(123, ‘hello’) returns a value of type [string, number].

exercises-8

Making address:exercises-8
The title
// index.ts
interface User {
  type: 'user'
  name: string
  age: number
  occupation: string
}

interface Admin {
  type: 'admin'
  name: string
  age: number
  role: string
}

/* Define the PowerUser type to have all the fields of User and Admin, where the type field is "PowerUser" */
type PowerUser = unknown

export type Person = User | Admin | PowerUser
Copy the code
answer

We need to refine the PowerUser type so that it has all the fields of User and Admin, and its type is “PowerUser”. Obviously, we need to use the joint type:

type PowerUser = User & Admin & { type: 'powerUser' } // The resulting type is any
Copy the code

If you do this, you’ll notice that the PowerUser type is changed to any. Why? If you look at the User and Admin types, you can see that they both contain type and that type is a specific string. Using the & operator is problematic, because a string cannot be both ‘User’ and ‘Admin’. What can you do? Delete type from User and Admin and merge them:

type PowerUser = Omit<User, 'type'> &
  Omit<Admin, 'type'> & {
    type: 'powerUser'
  }
Copy the code

That’s exactly what they’re saying, including an auxiliary generic Omit.

exercises-9

Making address:exercises-9
The title
// index.ts
/** This topic is too long, it takes up a lot of space, you can click on the link above to see, here I only posted the topic requirements: Remove the UsersApiResponse and AdminsApiResponse types and use the generic ApiResponse type to specify the format */ for the response for each API function
export type ApiResponse<T> = unknown
Copy the code
answer

We can start by looking at the format of the response data in question

type AdminsApiResponse =
  | {
      status: 'success'
      data: Admin[]
    }
  | {
      status: 'error'
      error: string
    }
type UsersApiResponse =
  | {
      status: 'success'
      data: User[]
    }
  | {
      status: 'error'
      error: string
    }
Copy the code

Each response data has a success or failure state. Data is returned on success and error is returned on failure. The error type is string, but data is different.

We need to write a generic type. Here we can use generics to refactor:

export type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string }
Copy the code

To use it, just pass in the generic:

export function requestAdmins(
  callback: (response: ApiResponse<Admin[]>) => void
) {
  callback({
    status: 'success'.data: admins
  })
}
Copy the code

The same is true for other functions.

The first few problems are relatively simple, but now let’s look at a slightly more complicated problem.

exercises-10

Making address:exercises-10
The title
// index.ts
Exercise: We don't want to re-implement all the data requests. Let's spruce up the old callback-based function, whose results are compatible with Promise. The function should eventually return a promise that resolves the final data or rejects an error, called promisify. More difficult exercise: Create a function promisifyAll that accepts objects with functions and returns a new object, each of which is promised. const api = promisifyAll(oldApi); * /
Copy the code
// test.ts
Copy the code
answer

We need to change the old callback based API to promise. Let’s look at the implementation of these apis:

const oldApi = {
  requestAdmins(callback: (response: ApiResponse<Admin[]>) => void) {
    callback({
      status: 'success'.data: admins
    })
  },
  requestUsers(callback: (response: ApiResponse<User[]>) => void) {
    callback({
      status: 'success'.data: users
    })
  },
  requestCurrentServerTime(callback: (response: ApiResponse<number= > >)void) {
    callback({
      status: 'success'.data: Date.now()
    })
  },
  requestCoffeeMachineQueueLength(
    callback: (response: ApiResponse<number= > >)void
  ) {
    callback({
      status: 'error'.error: 'Numeric value has exceeded Number.MAX_SAFE_INTEGER.'}}})export const api = {
  requestAdmins: promisify(oldApi.requestAdmins),
  requestUsers: promisify(oldApi.requestUsers),
  requestCurrentServerTime: promisify(oldApi.requestCurrentServerTime),
  requestCoffeeMachineQueueLength: promisify(
    oldApi.requestCoffeeMachineQueueLength
  )
}
Copy the code

We need to implement a promisify function that accepts an old API function and returns a new one. This new function is a promisify wrapped function that we can easily implement with JS:

export function promisify(fn) {
  return () = >
    new Promise((resolve, reject) = > {
      fn((response) = > {
        if (response.status === 'success') {
          resolve(response.data)
        } else {
          reject(new Error(response.error))
        }
      })
    })
}
Copy the code

Then we’ll change it to TS so that we can derive the type correctly. The promisify parameter fn is of the same type as oldApi’s function.

// The callback argument is response: ApiResponse
      
       . The generic T is the interface return value, such as Admin[].
      
type CallbackBasedAsyncFunction<T> = (
  callback: (response: ApiResponse<T>) => void
) = > void
Copy the code

Then determine that the return value of promisify, as mentioned above, should be a new function that returns a promise:

type PromiseBasedAsyncFunction<T> = () = > Promise<T>
Copy the code

The internal logic of the function is very simple, there is nothing to change, only remember to add the generic T when returning a new Promise, and then our promisify function looks like this:

export function promisify<T> (
  fn: CallbackBasedAsyncFunction<T>
) :PromiseBasedAsyncFunction<T> {
  return () = >
    new Promise<T>((resolve, reject) = > {
      fn((response) = > {
        if (response.status === 'success') {
          resolve(response.data)
        } else {
          reject(new Error(response.error))
        }
      })
    })
}
Copy the code

Then try again:

const requestAdmins = promisify(oldApi.requestAdmins)
Copy the code

Can get correct requestAdmins return value type PromiseBasedAsyncFunction < Admin [] >.

A more difficult exercise is to implement a promisifyAll function that changes all functions in the object to be promise-based.

To implement this function, we first need to determine what the parameters and return value of the function are. In fact, the parameters are oldAPi object, the type is oldAPi:

type OldApi = {
  requestAdmins(callback: (response: ApiResponse<Admin[]>) = > void) :void
  requestUsers(callback: (response: ApiResponse<User[]>) = > void) :void
  requestCurrentServerTime(
    callback: (response: ApiResponse<number>) = > void) :void
  requestCoffeeMachineQueueLength(
    callback: (response: ApiResponse<number>) = > void) :void
}
Copy the code

Callback (); callback (); callback (); callback ();

type ApiResponses = {
  requestAdmins: Admin[]
  requestUsers: User[]
  requestCurrentServerTime: number
  requestCoffeeMachineQueueLength: number
}

type SourceObject = {
  [K in keyof ApiResponses]: CallbackBasedAsyncFunction<ApiResponses[K]>
}
Copy the code

With the help of a problem here in creating the types of CallbackBasedAsyncFunction actually SourceObject can go further, the ApiResponses as generic:

type SourceObject<T> = { [K in keyof T]: CallbackBasedAsyncFunction<T[K]> }
Copy the code

We just need to pass in ApiResponses to get OldApi:

type OldApi = SourceObject<ApiResponses>
Copy the code

Now we get promisifyAll:

function promisifyAll<T> (obj: SourceObject<T>) :unknown {}
Copy the code

Here one might think that we need to pass in generic T, promisifyAll

(oldApi), which is not needed, the ApiResponses are just a product of our thinking, You just need to use the promisifyAll(oldApi) because generics are bi-directional. As soon as you pass oldApi, TS will derive the generic T as ApiResponses by itself.

The SourceObject iterates over T, which is an object by default, so we need to add a generic constraint to T:

function promisifyAll<T extends { [key: string] :any }>(
  obj: SourceObject<T>
): unknown {}
Copy the code

Then there is the return value type, had the SourceObject, the basis of the type of the return value is good writing, you can use on a question of CallbackBasedAsyncFunction type:

type SourceObject<T> = { [K in keyof T]: CallbackBasedAsyncFunction<T[K]> }
type PromisifiedObject<T> = { [K in keyof T]: PromiseBasedAsyncFunction<T[K]> }
function promisifyAll<T extends { [key: string] :any }>(
  obj: SourceObject<T>
): PromisifiedObject<T> {}
Copy the code

Then there is the internal logic of the function, in fact, the internal logic of the function is relatively simple, we first use JS to achieve a version:

function promisifyAll(obj) {
  const result = {}
  for (const key of Object.keys(obj)) {
    result[key] = promisify(obj[key])
  }
  return result
}
Copy the code

As long as the final return value is promisfieDoBject

, the final return value is promiseDobject

:

type SourceObject<T> = { [K in keyof T]: CallbackBasedAsyncFunction<T[K]> }
type PromisifiedObject<T> = { [K in keyof T]: PromiseBasedAsyncFunction<T[K]> }
function promisifyAll<T extends { [key: string] :any }>(
  obj: SourceObject<T>
): PromisifiedObject<T> {
  const result = {} as PromisifiedObject<T>
  for (const key of Object.keys(obj) as (keyof T)[]) {
    result[key] = promisify(obj[key])
  }
  return result
}
Copy the code

When using:

export const api = promisifyAll(oldApi)
Copy the code

Can get the API type correctly.

conclusion

Can you solve these problems successfully? It doesn’t matter if we can’t figure it out, we can learn over time. Here I recommend you to pull down the git repository code, there are 15 questions, from easy to difficult, you can start with the first question is the simplest, I believe you will learn something in the process.