TypeScript 練習題(第二彈)

TypeScript 的學習資料很是多,其中也不乏不少優秀的文章和教程。可是目前爲止沒有一個我特別滿意的。緣由有:html

  • 它們大多數沒有一個清晰的主線,而是按照 API 組織章節的,內容在邏輯上比較零散。
  • 大可能是「講是什麼,怎麼用「,而不是」講爲何,講原理「。
  • 大多數內容比較枯燥,趣味性比較低。都是乾巴巴的文字,沒有圖片,缺少可以引發強烈共鳴的例子。

所以個人想法是作一套不一樣市面上大多數的 TypeScript 學習教程。以人類認知的角度思考問題,學習 TypeScript,經過通俗易懂的例子和圖片來幫助你們創建 TypeScript 世界觀。前端

系列安排:node

目錄未來可能會有所調整。

注意,個人系列文章基本不會講 API,所以須要你有必定的 TypeScript 使用基礎,推薦兩個學習資料。git

結合這兩個資料和個人系列教程,掌握 TypeScript 指日可待。github

接下來,咱們經過幾個方面來從宏觀的角度來看一下 TypeScript。typescript

<!-- more -->編程

前言

本文涉及的題目一共十六道,所有均可以在 typescript-exercises 上在線提交。後端

能夠和標準答案進行對比。
api

而且因爲使用了瀏覽器緩存, 所以無需登陸的狀況下也能夠保證關掉頁面,你的答題進度也會保留。promise

想重置進度,清空緩存,無痕模式或者換瀏覽器均可以。

題目中涉及到的知識點我基本也都在以前的文章中提到了,若是你沒有看過,強烈建議先完成前面的教程,而後將上面的題目本身作一遍以後再看本文。另一定要按照順序讀, 所以前面的題目都是後面的鋪墊。

爲了避免讓文章太過於冗長, 本篇文章分兩次發佈, 一次 8 道題,一共十五道。每道題都有思路,前置知識以及代碼。 此次給你們帶來的是後 6 道

其中有一道題須要你們有函數式編程的知識, 若是你們不知道會比較難以解釋。 爲了不內容太過度散,將這道題從個人題解中移除,故只有 6 道。

題目九

題目描述

Intro:

    PowerUsers idea was bad. Once those users got
    extended permissions, they started bullying others
    and we lost a lot of great users.
    As a response we spent all the remaining money
    on the marketing and got even more users.
    We need to start preparing to move everything to a
    real database. For now we just do some mocks.

    The server API format was decided to be the following:

    In case of success: { status: 'success', data: RESPONSE_DATA }
    In case of error: { status: 'error', error: ERROR_MESSAGE }

    The API engineer started creating types for this API and
    quickly figured out that the amount of types needed to be
    created is too big.

Exercise:

    Remove UsersApiResponse and AdminsApiResponse types
    and use generic type ApiResponse in order to specify API
    response formats for each of the functions.

題目的大概意思是:以前都是寫死的數據, 如今數據須要從接口拿,請你定義這個接口的類型。

題目內置代碼

interface User {
  type: "user";
  name: string;
  age: number;
  occupation: string;
}

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

type Person = User | Admin;

const admins: Admin[] = [
  { type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
  { type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
];

const users: User[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  { type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
];

export type ApiResponse<T> = unknown;

type AdminsApiResponse =
  | {
      status: "success";
      data: Admin[];
    }
  | {
      status: "error";
      error: string;
    };

export function requestAdmins(callback: (response: AdminsApiResponse) => void) {
  callback({
    status: "success",
    data: admins,
  });
}

type UsersApiResponse =
  | {
      status: "success";
      data: User[];
    }
  | {
      status: "error";
      error: string;
    };

export function requestUsers(callback: (response: UsersApiResponse) => void) {
  callback({
    status: "success",
    data: users,
  });
}

export function requestCurrentServerTime(
  callback: (response: unknown) => void
) {
  callback({
    status: "success",
    data: Date.now(),
  });
}

export function requestCoffeeMachineQueueLength(
  callback: (response: unknown) => void
) {
  callback({
    status: "error",
    error: "Numeric value has exceeded Number.MAX_SAFE_INTEGER.",
  });
}

function logPerson(person: Person) {
  console.log(
    ` - ${person.name}, ${person.age}, ${
      person.type === "admin" ? person.role : person.occupation
    }`
  );
}

function startTheApp(callback: (error: Error | null) => void) {
  requestAdmins((adminsResponse) => {
    console.log("Admins:");
    if (adminsResponse.status === "success") {
      adminsResponse.data.forEach(logPerson);
    } else {
      return callback(new Error(adminsResponse.error));
    }

    console.log();

    requestUsers((usersResponse) => {
      console.log("Users:");
      if (usersResponse.status === "success") {
        usersResponse.data.forEach(logPerson);
      } else {
        return callback(new Error(usersResponse.error));
      }

      console.log();

      requestCurrentServerTime((serverTimeResponse) => {
        console.log("Server time:");
        if (serverTimeResponse.status === "success") {
          console.log(
            `   ${new Date(serverTimeResponse.data).toLocaleString()}`
          );
        } else {
          return callback(new Error(serverTimeResponse.error));
        }

        console.log();

        requestCoffeeMachineQueueLength((coffeeMachineQueueLengthResponse) => {
          console.log("Coffee machine queue length:");
          if (coffeeMachineQueueLengthResponse.status === "success") {
            console.log(`   ${coffeeMachineQueueLengthResponse.data}`);
          } else {
            return callback(new Error(coffeeMachineQueueLengthResponse.error));
          }

          callback(null);
        });
      });
    });
  });
}

startTheApp((e: Error | null) => {
  console.log();
  if (e) {
    console.log(
      `Error: "${e.message}", but it's fine, sometimes errors are inevitable.`
    );
  } else {
    console.log("Success!");
  }
});

前置知識

  • 泛型
  • 回調函數

思路

咱們仍是直接看報錯。

很明顯這個報錯的緣由是類型是 unknown, 所以咱們只有將 unknown 改爲正確的類型便可。

換句話說, 就是把這種地方改爲正確類型便可。

題目描述說了, 這個 response 實際上是從後端返回的。 然後端返回的數據有固定的格式。好比獲取用戶列表接口:

type UsersApiResponse =
  | {
      status: "success";
      data: User[];
    }
  | {
      status: "error";
      error: string;
    };

其餘接口也是相似, 不一樣的是 data 的類型。所以咱們考慮使用泛型封裝,將 data 的類型做爲參數便可。

從本質上來講, 就是從後端取的數據有兩種大的可能, 一種是錯誤, 一種是成功。二者在同一接口同一時刻只會出現一個,且必須出現一個。

而成功的狀況又會隨着接口不一樣從而可能產生不一樣的類型。

這是明顯的使用 或邏輯關係泛型進行類型定義的強烈信號。
咱們可使用泛型作以下改造:

export type ApiResponse<T> =
  | {
      status: "success";
      data: T;
    }
  | {
      status: "error";
      error: string;
    };

那麼上面的 UsersApiResponse 就能夠變成:

type UsersApiResponse = ApiResponse<User[]>;

不懂的同窗建議看下我以前的文章:- 你不知道的 TypeScript 泛型(萬字長文,建議收藏)

用一樣的套路把其餘後端返回加上類型便可。

代碼

核心代碼:

export type ApiResponse<T> =
  | {
      status: "success";
      data: T;
    }
  | {
      status: "error";
      error: string;
    };

export function requestAdmins(
  callback: (response: ApiResponse<Admin[]>) => void
) {
  callback({
    status: "success",
    data: admins,
  });
}

export function requestUsers(
  callback: (response: ApiResponse<User[]>) => void
) {
  callback({
    status: "success",
    data: users,
  });
}

export function requestCurrentServerTime(
  callback: (response: ApiResponse<number>) => void
) {
  callback({
    status: "success",
    data: Date.now(),
  });
}

export function requestCoffeeMachineQueueLength(
  callback: (response: ApiResponse<number>) => void
) {
  callback({
    status: "error",
    error: "Numeric value has exceeded Number.MAX_SAFE_INTEGER.",
  });
}

題目十

題目描述

Intro:

    We have asynchronous functions now, advanced technology.
    This makes us a tech startup officially now.
    But one of the consultants spoiled our dreams about
    inevitable future IT leadership.
    He said that callback-based asynchronicity is not
    popular anymore and everyone should use Promises.
    He promised that if we switch to Promises, this would
    bring promising results.

Exercise:

    We don't want to reimplement all the data-requesting
    functions. Let's decorate the old callback-based
    functions with the new Promise-compatible result.
    The final function should return a Promise which
    would resolve with the final data directly
    (i.e. users or admins) or would reject with an error
    (or type Error).

    The function should be named promisify.

Higher difficulty bonus exercise:

    Create a function promisifyAll which accepts an object
    with functions and returns a new object where each of
    the function is promisified.

    Rewrite api creation accordingly:

        const api = promisifyAll(oldApi);

題目大意是:前面用的是基於 callback 形式的代碼, 他們對代碼進行了重構,改形成了 Promise,讓你對基於 Promise 的接口進行類型定義。

題目內置代碼

interface User {
  type: "user";
  name: string;
  age: number;
  occupation: string;
}

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

type Person = User | Admin;

const admins: Admin[] = [
  { type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
  { type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
];

const users: User[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  { type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
];

export type ApiResponse<T> =
  | {
      status: "success";
      data: T;
    }
  | {
      status: "error";
      error: string;
    };

export function promisify(arg: unknown): unknown {
  return null;
}

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
  ),
};

function logPerson(person: Person) {
  console.log(
    ` - ${person.name}, ${person.age}, ${
      person.type === "admin" ? person.role : person.occupation
    }`
  );
}

async function startTheApp() {
  console.log("Admins:");
  (await api.requestAdmins()).forEach(logPerson);
  console.log();

  console.log("Users:");
  (await api.requestUsers()).forEach(logPerson);
  console.log();

  console.log("Server time:");
  console.log(
    `   ${new Date(await api.requestCurrentServerTime()).toLocaleString()}`
  );
  console.log();

  console.log("Coffee machine queue length:");
  console.log(`   ${await api.requestCoffeeMachineQueueLength()}`);
}

startTheApp().then(
  () => {
    console.log("Success!");
  },
  (e: Error) => {
    console.log(
      `Error: "${e.message}", but it's fine, sometimes errors are inevitable.`
    );
  }
);

前置知識

  • Promise
  • promisify
  • 泛型
  • 高階函數

思路

題目給了一個 promisefy, 而且類型都是 unknown,不難看出, 它就是想讓咱們改造 promisefy 使其不報錯, 並能正確推導類型。

export function promisify(arg: unknown): unknown {
  return null;
}

咱們先不考慮這個類型怎麼寫,先把 promiify 實現一下再說。這須要你有一點高階函數和 promise 的知識。因爲這不是本文的重點,所以不贅述。

export function promisify(fn) {
  return () =>
    new Promise((resolve, reject) => {
      fn((response) => {
        if (response.status === "success") resolve(response.data);
        else reject(response.error);
      });
    });
}

接下來,咱們須要給其增長類型簽名。

這個 fn 其實是一個函數,而且又接受一個 callback 做爲參數。 所以大概是這個樣子:

((something) = void) => void

這裏的 something 實際上咱們在上一節已經解決了,直接套用便可。代碼:

(callback: (response: ApiResponse<T>) => void) => void

總體代碼大概是:

export function promisify<T>(
  fn: (callback: (response: ApiResponse<T>) => void) => void
): () => Promise<T> {
  // 上面的實現
}

代碼

核心代碼:

export function promisify<T>(
  fn: (callback: (response: ApiResponse<T>) => void) => void
): () => Promise<T> {
  return () =>
    new Promise((resolve, reject) => {
      fn((response) => {
        if (response.status === "success") resolve(response.data);
        else reject(response.error);
      });
    });
}

第十一題

題目描述

Intro:

    In order to engage users in the communication with
    each other we have decided to decorate usernames
    in various ways. A brief search led us to a library
    called "str-utils". Bad thing is that it lacks
    TypeScript declarations.

Exercise:

    Check str-utils module implementation at:
    node_modules/str-utils/index.js
    node_modules/str-utils/README.md

    Provide type declaration for that module in:
    declarations/str-utils/index.d.ts

    Try to avoid duplicates of type declarations,
    use type aliases.

題目的意思是他們用到了一個庫 str-utils,這個庫的人又沒給咱們寫類型定義,因而咱們不得不去本身寫(好真實的例子啊)。

其實就是讓咱們實現如下函數的類型簽名:

import {
  strReverse,
  strToLower,
  strToUpper,
  strRandomize,
  strInvertCase,
} from "str-utils";

題目內置代碼

// declarations/str-utils/index.d.js
declare module "str-utils" {
  // export const ...
  // export function ...
}

// index.ts
import {
  strReverse,
  strToLower,
  strToUpper,
  strRandomize,
  strInvertCase,
} from "str-utils";

interface User {
  type: "user";
  name: string;
  age: number;
  occupation: string;
}

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

type Person = User | Admin;

const admins: Admin[] = [
  { type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
  { type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
  { type: "admin", name: "Steve", age: 40, role: "Steve" },
  { type: "admin", name: "Will Bruces", age: 30, role: "Overseer" },
  { type: "admin", name: "Superwoman", age: 28, role: "Customer support" },
];

const users: User[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  { type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
  { type: "user", name: "Moses", age: 70, occupation: "Desert guide" },
  { type: "user", name: "Superman", age: 28, occupation: "Ordinary person" },
  { type: "user", name: "Inspector Gadget", age: 31, occupation: "Undercover" },
];

const isAdmin = (person: Person): person is Admin => person.type === "admin";
const isUser = (person: Person): person is User => person.type === "user";

export const nameDecorators = [
  strReverse,
  strToLower,
  strToUpper,
  strRandomize,
  strInvertCase,
];

function logPerson(person: Person) {
  let additionalInformation: string = "";
  if (isAdmin(person)) {
    additionalInformation = person.role;
  }
  if (isUser(person)) {
    additionalInformation = person.occupation;
  }
  const randomNameDecorator =
    nameDecorators[Math.round(Math.random() * (nameDecorators.length - 1))];
  const name = randomNameDecorator(person.name);
  console.log(` - ${name}, ${person.age}, ${additionalInformation}`);
}

([] as Person[]).concat(users, admins).forEach(logPerson);

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules

前置知識

  • 如何給缺少類型定義的第三方庫定義類型

思路

這個題目的考點就是如何給缺少類型定義的第三方庫定義類型

這個時候咱們只要新建一個文件而後加入如下代碼便可。

declare module "str-utils" {
  // 在這裏定義類型
  // export const ...
  // export function ...
}

其中 str-utils 是那個可惡的沒有類型定義的庫的名字。

有了這個知識,咱們的代碼就簡單了。

代碼

declare module "str-utils" {
  // export const ...
  // export function ...
  export function strReverse(s: string): string;
  export function strToLower(s: string): string;
  export function strToUpper(s: string): string;
  export function strRandomize(s: string): string;
  export function strInvertCase(s: string): string;
}

第十二題

題目描述

Intro:

    We have so many users and admins in the database!
    CEO's father Jeff says that we are a BigData
    startup now. We have no idea what it means, but
    Jeff says that we need to do some statistics and
    analytics.

    We've ran a questionnaire within the team to
    figure out what do we know about statistics.
    The only person who filled it was our coffee
    machine maintainer. The answers were:

     * Maximums
     * Minumums
     * Medians
     * Averages

    We found a piece of code on stackoverflow and
    compiled it into a module `stats`. The bad
    thing is that it lacks type declarations.

Exercise:

    Check stats module implementation at:
    node_modules/stats/index.js
    node_modules/stats/README.md

    Provide type declaration for that module in:
    declarations/stats/index.d.ts

Higher difficulty bonus exercise:

    Avoid duplicates of type declarations.

題目大概意思是又來了一個庫,這個庫又沒有寫定義,咱們又要本身寫。 (真實++)

題目內置代碼

// declartions/stats/index.d.ts
declare module "stats" {
  export function getMaxIndex(input: unknown, comparator: unknown): unknown;
}

// index.ts
import {
  getMaxIndex,
  getMaxElement,
  getMinIndex,
  getMinElement,
  getMedianIndex,
  getMedianElement,
  getAverageValue,
} from "stats";

interface User {
  type: "user";
  name: string;
  age: number;
  occupation: string;
}

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

const admins: Admin[] = [
  { type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
  { type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
  { type: "admin", name: "Steve", age: 40, role: "Steve" },
  { type: "admin", name: "Will Bruces", age: 30, role: "Overseer" },
  { type: "admin", name: "Superwoman", age: 28, role: "Customer support" },
];

const users: User[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  { type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
  { type: "user", name: "Moses", age: 70, occupation: "Desert guide" },
  { type: "user", name: "Superman", age: 28, occupation: "Ordinary person" },
  { type: "user", name: "Inspector Gadget", age: 31, occupation: "Undercover" },
];

function logUser(user: User | null) {
  if (!user) {
    console.log(" - none");
    return;
  }
  const pos = users.indexOf(user) + 1;
  console.log(` - #${pos} User: ${user.name}, ${user.age}, ${user.occupation}`);
}

function logAdmin(admin: Admin | null) {
  if (!admin) {
    console.log(" - none");
    return;
  }
  const pos = admins.indexOf(admin) + 1;
  console.log(` - #${pos} Admin: ${admin.name}, ${admin.age}, ${admin.role}`);
}

const compareUsers = (a: User, b: User) => a.age - b.age;
const compareAdmins = (a: Admin, b: Admin) => a.age - b.age;
const colorizeIndex = (value: number) => String(value + 1);

export {
  getMaxIndex,
  getMaxElement,
  getMinIndex,
  getMinElement,
  getMedianIndex,
  getMedianElement,
  getAverageValue,
};

console.log("Youngest user:");
logUser(getMinElement(users, compareUsers));
console.log(
  ` - was ${colorizeIndex(getMinIndex(users, compareUsers))}th to register`
);

console.log();

console.log("Median user:");
logUser(getMedianElement(users, compareUsers));
console.log(
  ` - was ${colorizeIndex(getMedianIndex(users, compareUsers))}th to register`
);

console.log();

console.log("Oldest user:");
logUser(getMaxElement(users, compareUsers));
console.log(
  ` - was ${colorizeIndex(getMaxIndex(users, compareUsers))}th to register`
);

console.log();

console.log("Average user age:");
console.log(
  ` - ${String(getAverageValue(users, ({ age }: User) => age))} years`
);

console.log();

console.log("Youngest admin:");
logAdmin(getMinElement(admins, compareAdmins));
console.log(
  ` - was ${colorizeIndex(getMinIndex(users, compareUsers))}th to register`
);

console.log();

console.log("Median admin:");
logAdmin(getMedianElement(admins, compareAdmins));
console.log(
  ` - was ${colorizeIndex(getMedianIndex(users, compareUsers))}th to register`
);

console.log();

console.log("Oldest admin:");
logAdmin(getMaxElement(admins, compareAdmins));
console.log(
  ` - was ${colorizeIndex(getMaxIndex(users, compareUsers))}th to register`
);

console.log();

console.log("Average admin age:");
console.log(
  ` - ${String(getAverageValue(admins, ({ age }: Admin) => age))} years`
);

前置知識

  • 泛型
  • 高階函數
  • 如何給缺少類型定義的第三方庫定義類型

思路

和上面的思路相似。 惟一的不一樣的是這道題的須要實現的幾個方法支持不一樣的入參類型。

import {
  getMaxIndex,
  getMaxElement,
  getMinIndex,
  getMinElement,
  getMedianIndex,
  getMedianElement,
  getAverageValue,
} from "stats";

所以,咱們考慮使用泛型來定義。 知道了這個, 代碼就不難寫。 這是最最基本的泛型, 比咱們前面寫的還簡單。

代碼

declare module "stats" {
  export function getMaxIndex<T>(
    input: T[],
    comparator: (a: T, b: T) => number
  ): number;
  export function getMaxElement<T>(
    input: T[],
    comparator: (a: T, b: T) => number
  ): T;
  export function getMinElement<T>(
    input: T[],
    comparator: (a: T, b: T) => number
  ): T;
  export function getMedianIndex<T>(
    input: T[],
    comparator: (a: T, b: T) => number
  ): number;
  export function getMedianElement<T>(
    input: T[],
    comparator: (a: T, b: T) => number
  ): T;
  export function getAverageValue<T>(
    input: T[],
    getValue: (a: T) => number
  ): number;
  export function getMinIndex<T>(
    input: T[],
    comparator: (a: T, b: T) => number
  ): number;
}

第十三題

題目描述

Intro:

    The next logical step for us is to provide more
    precise registration date for our users and admins.
    We've approximately made up dates for each user and
    admin and used a library called "date-wizard" in
    order to pretty-format the dates.

    Unfortunately, type declarations which came with
    "date-wizard" library were incomplete.

    1. DateDetails interface is missing
       time related fields such as hours, minutes and
       seconds.
    2. Function "pad" is exported but not declared.

Exercise:

    Check date-wizard module implementation at:
    node_modules/date-wizard/index.js
    node_modules/date-wizard/index.d.ts

    Extend type declaration of that module in:
    module-augmentations/date-wizard/index.ts

題目大概意思是又來了一個庫,這個庫又沒有寫定義,咱們又要本身寫。 (真實+++++++++++++)

題目內置代碼

// module-augmentations/data-wizard/index.d.ts

// This enables module augmentation mode.
import "date-wizard";

declare module "date-wizard" {
  // Add your module extensions here.
}

// index.ts
import * as dateWizard from "date-wizard";
import "./module-augmentations/date-wizard";

interface User {
  type: "user";
  name: string;
  age: number;
  occupation: string;
  registered: Date;
}

interface Admin {
  type: "admin";
  name: string;
  age: number;
  role: string;
  registered: Date;
}

type Person = User | Admin;

const admins: Admin[] = [
  {
    type: "admin",
    name: "Jane Doe",
    age: 32,
    role: "Administrator",
    registered: new Date("2016-06-01T16:23:13"),
  },
  {
    type: "admin",
    name: "Bruce Willis",
    age: 64,
    role: "World saver",
    registered: new Date("2017-02-11T12:12:11"),
  },
  {
    type: "admin",
    name: "Steve",
    age: 40,
    role: "Steve",
    registered: new Date("2018-01-05T11:02:30"),
  },
  {
    type: "admin",
    name: "Will Bruces",
    age: 30,
    role: "Overseer",
    registered: new Date("2018-08-12T10:01:24"),
  },
  {
    type: "admin",
    name: "Superwoman",
    age: 28,
    role: "Customer support",
    registered: new Date("2019-03-25T07:51:05"),
  },
];

const users: User[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
    registered: new Date("2016-02-15T09:25:13"),
  },
  {
    type: "user",
    name: "Kate Müller",
    age: 23,
    occupation: "Astronaut",
    registered: new Date("2016-03-23T12:47:03"),
  },
  {
    type: "user",
    name: "Moses",
    age: 70,
    occupation: "Desert guide",
    registered: new Date("2017-02-19T17:22:56"),
  },
  {
    type: "user",
    name: "Superman",
    age: 28,
    occupation: "Ordinary person",
    registered: new Date("2018-02-25T19:44:28"),
  },
  {
    type: "user",
    name: "Inspector Gadget",
    age: 31,
    occupation: "Undercover",
    registered: new Date("2019-03-25T09:29:12"),
  },
];

const isAdmin = (person: Person): person is Admin => person.type === "admin";
const isUser = (person: Person): person is User => person.type === "user";

function logPerson(person: Person, index: number) {
  let additionalInformation: string = "";
  if (isAdmin(person)) {
    additionalInformation = person.role;
  }
  if (isUser(person)) {
    additionalInformation = person.occupation;
  }
  let registeredAt = dateWizard(
    person.registered,
    "{date}.{month}.{year} {hours}:{minutes}"
  );
  let num = `#${dateWizard.pad(index + 1)}`;
  console.log(
    ` - ${num}: ${person.name}, ${person.age}, ${additionalInformation}, ${registeredAt}`
  );
}

export { dateWizard };

console.log("All users:");

([] as Person[]).concat(users, admins).forEach(logPerson);

console.log();

console.log("Early birds:");

([] as Person[])
  .concat(users, admins)
  .filter((person) => dateWizard.dateDetails(person.registered).hours < 10)
  .forEach(logPerson);

// In case if you are stuck:
// https://www.typescriptlang.org/docs/handbook/modules.html#ambient-modules
// https://www.typescriptlang.org/docs/handbook/declaration-merging.html

前置知識

  • interface 或 type 聲明自定義類型
  • 如何給缺少類型定義的第三方庫定義類型

思路

和上面兩道題思路同樣, 不用多說了吧?

代碼

// This enables module augmentation mode.
import "date-wizard";

declare module "date-wizard" {
  // Add your module extensions here.
  function dateWizard(date: string, format: string): string;
  function pad(s: number): string;
  interface DateDetails {
    year: number;
    month: number;
    date: number;
    hours: number;
    minutes: number;
    seconds: number;
  }
  function dateDetails(date: Date): DateDetails;
}

第十四題

須要你們有函數式編程的知識, 若是你們不知道會比較難以解釋。 爲了不內容太過度散,將這道題從個人題解中移除。

對函數式編程感興趣的,也但是看下我以前寫的文章 函數式編程系列教程

第十五題

題目描述

Intro:

    Our attempt to Open Source didn't work quite as
    expected. It turned out there were already many
    existing functional JS libraries.

    All the remaining developers left the company as
    well. It seems that they are joining a very
    ambitious startup which re-invented a juicer and
    raised millions of dollars.
    Too bad we cannot compete with this kind of
    financing even though we believe our idea is
    great.

    It's time to shine for the last time and publish
    our new invention: object-constructor as our CTO
    named it. A small library which helps
    manipulating an object.

Exercise:

    Here is a library which helps manipulating objects.
    We tried to write type annotations and we failed.
    Please help!

題目大概意思是函數式編程他們 hold 不住,因而又準備切換到面向對象編程。 因而你須要補充類型定義使得代碼不報錯。

題目內置代碼

export class ObjectManipulator {
  constructor(protected obj) {}

  public set(key, value) {
    return new ObjectManipulator({ ...this.obj, [key]: value });
  }

  public get(key) {
    return this.obj[key];
  }

  public delete(key) {
    const newObj = { ...this.obj };
    delete newObj[key];
    return new ObjectManipulator(newObj);
  }

  public getObject() {
    return this.obj;
  }
}

前置知識

  • 泛型
  • Omit 泛型
  • ES6 class
  • keyof
  • 使用 extends 進行泛型約束
  • 聯合類型

思路

這道題難度頗高,比前面的泛型題目都要難。 也是本系列的壓軸題,咱們重點講一下。

首先題目有五個報錯位置, 報錯信息都是隱式使用了 any , 所以咱們的思路就是將五個地方顯式聲明類型便可。

從它的名字 ObjectManipulator 以及 api 能夠看出, 它應該能夠存儲任何對象,所以使用泛型定義就不難想到。

你也但是把這個 ObjectManipulator 想象成抽象包包。 你的指望是限量款包包拍照的時候用,普通包包和閨蜜逛街的時候用,優衣庫送的包包逛超市的時候用等等。

ObjectManipulator 是一個抽象的包包概念,不是具體的包, 好比當你買一個 LV 的包包的時候就是 ObjectManipulator<LVBag>。這樣當你往 LV 裏放超市買的水果的時候就能夠報錯:你怎麼能夠用 LV 包包裝這樣東西呢?你應該用 ta 裝*

固然這個例子很不嚴謹, 這個只是幫助你們快速理解而已,切莫較真。

理解了題意,咱們就能夠開始寫了。

咱們先改第一個錯 - 構造函數 constructor, 這個錯比較簡單。

export class ObjectManipulator<T> {
  constructor(protected obj: T) {
    this.obj = obj;
  }
  ...
}

這個時候通過 ObjectManipulator 實例化產生的對象的 this.obj 都是 T 類型,其中 T 是泛型。所以 getObject 的錯也不難改,返回值寫 T 就行。

export class ObjectManipulator<T> {
  ...
  public getObject(): T {
    return this.obj;
  }
}

剩下的 get,set 和 delete 思路有點相似。 先拿 get 來講:

export class ObjectManipulator<T> {
  ...
  public get(key) {
    return this.obj[key];
  }
  ...
}

這個怎麼寫類型呢? key 理論上但是是任何值,返回值理論上也能夠是任何值。可是一旦類型 T 肯定了, 那麼實際上 key 和返回值就不是任意值了。 好比:

type A = ObjectManipulator<{ name: string; age: number }>;
const a: A = new ObjectManipulator({ name: "", age: 17 });

如上代碼中的 A 是 ObjectManipulator 傳入具體類型 { name: string; age: number } 產生的新的類型。

我這裏用的是行內類型, 實際項目建議使用 interface 或者 type 定義類型。

以後咱們模擬一些操做:

a.set("name", "腦洞前端");
a.get("name");
a.get("name123"); // 指望報錯
a.set("name123", "腦洞");
a.delete("name123"); // 指望報錯
a.delete("name");

實際上,我可能指望的是其中一些行爲能夠藉助 TypeScript 的類型分析直接報錯。

簡單來講,個人指望是 get 和 delete 不在 T 中的 key 都報錯。

固然你的真實項目也能夠不認同個人觀點, 好比 get 一個不在 T 中定義的 key 也能夠,可是我仍是推薦你這麼作。

知道了這個, 再結合我以前有關泛型的文章就不難寫出來。

其中 get 和 delete 的代碼:

export class ObjectManipulator<T> {
  public get<K extends keyof T>(key: K): T[K] {
    return this.obj[key];
  }

  public delete<K extends keyof T>(key: K): ObjectManipulator<Omit<T, K>> {
    const newObj = { ...this.obj };
    delete newObj[key];
    return new ObjectManipulator(newObj);
  }
}

最後是 set,其實一開始個人 set 是這麼寫的。

export class ObjectManipulator<T> {
  public set<K extends keyof T, V>(key: K, value: V): ObjectManipulator<T> {
    return new ObjectManipulator({
      ...this.obj,
      [key]: value,
    }) as ObjectManipulator<T & { [k in K]: V }>;
  }
}

可是無奈沒有經過官方的測試用例。 實際項目我其實更推薦我上面的這種寫法。下面是我爲了經過全部的測試用例寫的方法。

通過分析, 我發現它指望的是 set 中的 key 能夠不是 T 中的。這一點從官方給的測試用例就能夠看出來。

所以我將代碼改爲 K 放寬到任意 string,返回值作了一個聯合類型。代碼:

export class ObjectManipulator<T> {
  ...
  public set<K extends string, V>(
    key: K,
    value: V
  ): ObjectManipulator<T & { [k in K]: V }> {
    return new ObjectManipulator({
      ...this.obj,
      [key]: value,
    }) as ObjectManipulator<T & { [k in K]: V }>;
  }
  ...
}

終於經過了全部的測試用例。

代碼

export class ObjectManipulator<T> {
  constructor(protected obj: T) {
    this.obj = obj;
  }
  public set<K extends string, V>(
    key: K,
    value: V
  ): ObjectManipulator<T & { [k in K]: V }> {
    return new ObjectManipulator({
      ...this.obj,
      [key]: value,
    }) as ObjectManipulator<T & { [k in K]: V }>;
  }

  public get<K extends keyof T>(key: K): T[K] {
    return this.obj[key];
  }

  public delete<K extends keyof T>(key: K): ObjectManipulator<Omit<T, K>> {
    const newObj = { ...this.obj };
    delete newObj[key];
    return new ObjectManipulator(newObj);
  }

  public getObject(): T {
    return this.obj;
  }
}

總結

以上就是給你們帶來的題目解析。 這六道題的考點有,按照我我的理解的重要程度劃分爲:

  • type 和 interface 的基本操做(必須掌握)
  • 如何給缺少類型定義的第三方庫定義類型(必須掌握)
  • 聯合類型 和 交叉類型(強烈建議掌握)
  • 類型斷言和類型收縮(強烈建議掌握)
  • 泛型和常見內置泛型(強烈建議掌握)
  • 高階函數的類型定義(強烈建議掌握)

最後祝願你們告別 anyscript,成爲 TypeScript 魔法師。

關注我

你們也能夠關注個人公衆號《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認識你不知道的前端。

相關文章
相關標籤/搜索