TypeScript 練習題

lucifer發表於2020-09-28

TypeScript 的學習資料非常多,其中也不乏很多優秀的文章和教程。但是目前為止沒有一個我特別滿意的。原因有:

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

因此我的想法是做一套不同市面上大多數的 TypeScript 學習教程。以人類認知的角度思考問題,學習 TypeScript,通過通俗易懂的例子和圖片來幫助大家建立 TypeScript 世界觀。

系列安排:

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

注意,我的系列文章基本不會講 API,因此需要你有一定的 TypeScript 使用基礎,推薦兩個學習資料。

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

接下來,我們通過幾個方面來從巨集觀的角度來看一下 TypeScript。

<!-- more -->

前言

本文涉及的題目一共十六道,全部都可以在 typescript-exercises 上線上提交。

可以和標準答案進行對比。

並且由於使用了瀏覽器快取, 因此無需登入的情況下也可以保證關掉頁面,你的答題進度也會保留。

想重置進度,清空快取,無痕模式或者換瀏覽器都可以。

題目中涉及到的知識點我基本也都在之前的文章中提到了,如果你沒有看過,強烈建議先完成前面的教程,然後將上面的題目自己做一遍之後再看本文。

為了不讓文章太過於冗長, 本篇文章分兩次釋出, 一次 8 道題,一共十六道。每道題都有思路,前置知識以及程式碼。

題目一

題目描述

Intro:

    We are starting a small community of users. For performance
    reasons we have decided to store all users right in the code.
    This way we can provide our developers with more
    user-interaction opportunities. With user-related data, at least.
    All the GDPR-related issues we will solved some other day.
    This would be the base for our future experiments during
    these exercises.

Exercise:

    Given the data, define the interface "User" and use it accordingly.

題目的大概意思是讓你定義一個型別 User, 使得程式碼可以正常執行。

題目內建程式碼

export type User = unknown;

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

export function logPerson(user: unknown) {
  console.log(` - ${user.name}, ${user.age}`);
}

console.log("Users:");
users.forEach(logPerson);

前置知識

  • interface 或 type 宣告自定義型別

思路

這道題比較簡單, 我們只有定義一個 User 類即可。從 users 陣列中不難看出, User 中有三個屬性 name ,age 和 occupation,型別分別為 string, number 和 string。因此直接使用 type 或者 interface 定義自定義型別即可。

程式碼

核心程式碼:

export type User = {
  name: string;
  age: number;
  occupation: string;
};

題目二

題目描述

Intro:

    All 2 users liked the idea of the community. We should go
    forward and introduce some order. We are in Germany after all.
    Let's add a couple of admins.

    Initially we only had users in the in-memory database. After
    introducing Admins, we need to fix the types so that
    everything works well together.

Exercise:

    Type "Person" is missing, please define it and use
    it in persons array and logPerson function in order to fix
    all the TS errors.

題目大意是補充 Person 類, 使得程式碼不報錯。

題目內建程式碼

interface User {
  name: string;
  age: number;
  occupation: string;
}

interface Admin {
  name: string;
  age: number;
  role: string;
}

export type Person = unknown;

export const persons: User[] /* <- Person[] */ = [
  {
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  {
    name: "Jane Doe",
    age: 32,
    role: "Administrator",
  },
  {
    name: "Kate Müller",
    age: 23,
    occupation: "Astronaut",
  },
  {
    name: "Bruce Willis",
    age: 64,
    role: "World saver",
  },
];

export function logPerson(user: User) {
  console.log(` - ${user.name}, ${user.age}`);
}

persons.forEach(logPerson);

前置知識

  • 聯合型別

思路

我們直接從報錯入手。

不難發現 persons 陣列既有 User 又有 Admin。 因此 person 的函式簽名應該是兩者的聯合型別。而題目又讓我們補充 Person,於是程式碼將 Person 定義為 Admin 和 User 的聯合型別就不難想到。

程式碼

核心程式碼:

export type Person = User | Admin;

這個時候, persons 陣列使用的過程只能用 User 和 Admin 的共有屬性, 也就是 name 和 age,這點後面的題目也會提到。 因此如果你使用了 role 或者 occupation 就會報錯。怎麼解決呢? 我們繼續看下一題。

第三題

題目描述

Intro:

    Since we already have some of the additional
    information about our users, it's a good idea
    to output it in a nice way.

Exercise:

    Fix type errors in logPerson function.

    logPerson function should accept both User and Admin
    and should output relevant information according to
    the input: occupation for User and role for Admin.

題目內建程式碼

interface User {
  name: string;
  age: number;
  occupation: string;
}

interface Admin {
  name: string;
  age: number;
  role: string;
}

export type Person = User | Admin;

export const persons: Person[] = [
  {
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  {
    name: "Jane Doe",
    age: 32,
    role: "Administrator",
  },
  {
    name: "Kate Müller",
    age: 23,
    occupation: "Astronaut",
  },
  {
    name: "Bruce Willis",
    age: 64,
    role: "World saver",
  },
];

export function logPerson(person: Person) {
  let additionalInformation: string;
  if (person.role) {
    additionalInformation = person.role;
  } else {
    additionalInformation = person.occupation;
  }
  console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}

persons.forEach(logPerson);

前置知識

  • 型別斷言
  • 型別收斂
  • in 操作符

思路

關於型別收斂, 我在 TypeScript 型別系統 做了很詳情的討論。

上面程式碼報錯的原因前面已經講過了, 那麼如何解決呢?由於 person 可能是 User ,也可能是 Admin 型別,而 TypeScript 沒有足夠的資訊確定具體是哪一種。因此你使用 User 或者 Admin 特有的屬性就會報錯了。

因此解決方案的基本思想就是告訴 TypeScript person 當前是 Admin 還是 User 型別。有多種方式可以解決這個問題。

  1. 將 person 斷言為準確的型別。 就是告訴 TypeScript ”交給我吧, person 就是 xxx 型別,有錯就我的鍋“。

程式碼:

if ((<Admin>person).role) {
  additionalInformation = (<Admin>person).role;
} else {
  additionalInformation = (<User>person).occupation;
}
  1. 另外一種方式是使用型別收縮,比如 is , in, typeof , instanceof 等。使得 Typescript 能夠 Get 到當前的型別。”哦, person 上有 role 屬性啊,那它就是 Admin 型別,有問題我 Typescript 的鍋“

這裡我們使用 in 操作符,寫起來也很簡單。

推薦哪種不用我多說了吧 ?

程式碼

if ("role" in person) {
  // person 會被自動推導為 Admin
  additionalInformation = person.role;
} else {
  // Person 會被自動推導為 User
  additionalInformation = person.occupation;
}

第四題

題目描述

Intro:

    As we introduced "type" to both User and Admin
    it's now easier to distinguish between them.
    Once object type checking logic was extracted
    into separate functions isUser and isAdmin -
    logPerson function got new type errors.

Exercise:

    Figure out how to help TypeScript understand types in
    this situation and apply necessary fixes.

大概意思還是讓你改程式碼, 使得 Typescript 能理解(不報錯)。

題目內建程式碼

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

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

export type Person = User | Admin;

export const persons: Person[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  { type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
  { type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
  { type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
];

export function isAdmin(person: Person) {
  return person.type === "admin";
}

export function isUser(person: Person) {
  return person.type === "user";
}

export function logPerson(person: Person) {
  let additionalInformation: string = "";
  if (isAdmin(person)) {
    additionalInformation = person.role;
  }
  if (isUser(person)) {
    additionalInformation = person.occupation;
  }
  console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}

console.log("Admins:");
persons.filter(isAdmin).forEach(logPerson);

console.log();

console.log("Users:");
persons.filter(isUser).forEach(logPerson);

前置知識

  • 型別收斂
  • is 操作符

思路

我們仍然從報錯入手。

實際上還是 person 的型別問題, 沒有被收縮到正確的型別。看題目的程式碼,期望效果應該是如果進入 isAdmin 內部,那麼 person 就是 Admin 型別,同理進入 isUser 內部,那麼 person 就是 User 型別。

繼續看下 isAdmin 和 isUser 的實現:

export function isAdmin(person: Person) {
  return person.type === "admin";
}

export function isUser(person: Person) {
  return person.type === "user";
}

這裡我們期望的效果是如果 isAdmin 函式返回 true ,那麼 person 就應該被收斂為 Admin,isUser 同理。

這裡就需要用到 is 操作符。

上文提到了型別收斂常見的操作符是 is , in, typeof , instanceof

程式碼

export function isAdmin(person: Person): person is Admin {
  return person.type === "admin";
}

export function isUser(person: Person): person is User {
  return person.type === "user";
}

這樣當 isAdmin 返回 true, 那麼 person 變數就會被推導成 Admin 型別,而不是聯合型別, 也就是型別發生了收縮。

不難看出,這樣的型別斷言會直接影響到呼叫 isAdmin 或 isUser 的函式的入參的型別

第五題

題目描述

Intro:

    Time to filter the data! In order to be flexible
    we filter users using a number of criteria and
    return only those matching all of the criteria.
    We don't need Admins yet, we only filter Users.

Exercise:

    Without duplicating type structures, modify
    filterUsers function definition so that we can
    pass only those criteria which are needed,
    and not the whole User information as it is
    required now according to typing.

Higher difficulty bonus exercise:

    Exclude "type" from filter criterias.

大概意思是讓你改 filterUsers, 但要注意 DRY(Don't Repeat Yourself)。

題目內建程式碼

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

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

export type Person = User | Admin;

export const persons: Person[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  {
    type: "admin",
    name: "Jane Doe",
    age: 32,
    role: "Administrator",
  },
  {
    type: "user",
    name: "Kate Müller",
    age: 23,
    occupation: "Astronaut",
  },
  {
    type: "admin",
    name: "Bruce Willis",
    age: 64,
    role: "World saver",
  },
  {
    type: "user",
    name: "Wilson",
    age: 23,
    occupation: "Ball",
  },
  {
    type: "admin",
    name: "Agent Smith",
    age: 23,
    role: "Administrator",
  },
];

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

export function logPerson(person: Person) {
  let additionalInformation = "";
  if (isAdmin(person)) {
    additionalInformation = person.role;
  }
  if (isUser(person)) {
    additionalInformation = person.occupation;
  }
  console.log(` - ${person.name}, ${person.age}, ${additionalInformation}`);
}

export function filterUsers(persons: Person[], criteria: User): User[] {
  return persons.filter(isUser).filter((user) => {
    const criteriaKeys = Object.keys(criteria) as (keyof User)[];
    return criteriaKeys.every((fieldName) => {
      return user[fieldName] === criteria[fieldName];
    });
  });
}

console.log("Users of age 23:");

filterUsers(persons, {
  age: 23,
}).forEach(logPerson);

前置知識

  • 泛型
  • Partial 泛型

思路

老規矩, 從報錯入手。

大概意思是 { age: 23 } 不完整,缺失了部分 key。而題目實際上的想法應該是想根據部分內容對人員進行檢錯。比如可以根據 age 查, 也可以根據 name 查,也可以同時根據 age 和 name 查等,這和我們平時的搜尋邏輯是一致的。

直接用 Partial 泛型即可解決, 不懂的可以看下我的文章你不知道的 TypeScript 泛型(萬字長文,建議收藏)

程式碼

export function filterUsers(persons: Person[], criteria: Partial<User>): User[] {
    ...
}

第六題

題目描述

Intro:

    Filtering requirements have grown. We need to be
    able to filter any kind of Persons.

Exercise:

    Fix typing for the filterPersons so that it can filter users
    and return User[] when personType='user' and return Admin[]
    when personType='admin'. Also filterPersons should accept
    partial User/Admin type according to the personType.
    `criteria` argument should behave according to the
    `personType` argument value. `type` field is not allowed in
    the `criteria` field.

Higher difficulty bonus exercise:

    Implement a function `getObjectKeys()` which returns more
    convenient result for any argument given, so that you don't
    need to cast it.

    let criteriaKeys = Object.keys(criteria) as (keyof User)[];
    -->
    let criteriaKeys = getObjectKeys(criteria);

大概意思是讓你改 filterUsers, 但要注意 DRY(Don't Repeat Yourself)。並且可以根據 personType 的不同,返回不同的型別。

題目內建程式碼

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

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

export type Person = User | Admin;

export const persons: Person[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  { type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
  { type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
  { type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
  { type: "user", name: "Wilson", age: 23, occupation: "Ball" },
  { type: "admin", name: "Agent Smith", age: 23, role: "Anti-virus engineer" },
];

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

export function filterPersons(
  persons: Person[],
  personType: "admin",
  criteria: Partial<Person>
): Admin[];
export function filterPersons(
  persons: Person[],
  personType: "user",
  criteria: Partial<Person>
): User[];
export function filterPersons(
  persons: Person[],
  personType: string,
  criteria: Partial<Person>
): Person[] {
  return persons
    .filter((person) => person.type === personType)
    .filter((person) => {
      let criteriaKeys = Object.keys(criteria) as (keyof Person)[];
      return criteriaKeys.every((fieldName) => {
        return person[fieldName] === criteria[fieldName];
      });
    });
}

export const usersOfAge23 = filterPersons(persons, "user", { age: 23 });
export const adminsOfAge23 = filterPersons(persons, "admin", { age: 23 });

console.log("Users of age 23:");
usersOfAge23.forEach(logPerson);

console.log();

console.log("Admins of age 23:");
adminsOfAge23.forEach(logPerson);

前置知識

  • 泛型
  • Partial 泛型
  • 函式過載

思路

題目描述也懶得看了, 直接看報錯。

報錯資訊提示我們沒有找到合適的函式過載。 因此我的思路就是補上合適的過載即可。關於函式過載,我的系列教程不涉及,大家可以看下官網資料。

過載之後,不同的情況呼叫返回值就可以對應不同的型別。本題中就是:

  • 如果 personType 是 admin,就會返回 Admin 陣列。
  • 如果 personType 是 user,就會返回 User 陣列。
  • 如果 personType 是其他 string,就會返回 Person 陣列。

程式碼

export function filterPersons(persons: Person[], personType: 'admin', criteria: Partial<Person>): Admin[]
export function filterPersons(persons: Person[], personType: 'user', criteria: Partial<Person>): User[]
export function filterPersons(persons: Person[], personType: string, criteria: Partial<Person>): Person[] {
    ...
}

第七題

題目描述

Intro:

    Filtering was completely removed from the project.
    It turned out that this feature was just not needed
    for the end-user and we spent a lot of time just because
    our office manager told us to do so. Next time we should
    instead listen to the product management.

    Anyway we have a new plan. CEO's friend Nick told us
    that if we randomly swap user names from time to time
    in the community, it would be very funny and the project
    would definitely succeed!

Exercise:

    Implement swap which receives 2 persons and returns them in
    the reverse order. The function itself is already
    there, actually. We just need to provide it with proper types.
    Also this function shouldn't necessarily be limited to just
    Person types, lets type it so that it works with any two types
    specified.

題目大概意思是讓你修改 swap 函式,使得不報錯。 並且,我希望這個函式可以適用於任意兩個變數,不管其型別一樣不一樣, 也不管二者型別是什麼。

題目內建程式碼

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

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

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

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

const admins: Admin[] = [
  {
    type: "admin",
    name: "Will Bruces",
    age: 30,
    role: "Overseer",
  },
  {
    type: "admin",
    name: "Steve",
    age: 40,
    role: "Steve",
  },
];

const users: User[] = [
  {
    type: "user",
    name: "Moses",
    age: 70,
    occupation: "Desert guide",
  },
  {
    type: "user",
    name: "Superman",
    age: 28,
    occupation: "Ordinary person",
  },
];

export function swap(v1, v2) {
  return [v2, v1];
}

function test1() {
  console.log("test1:");
  const [secondUser, firstAdmin] = swap(admins[0], users[1]);
  logUser(secondUser);
  logAdmin(firstAdmin);
}

function test2() {
  console.log("test2:");
  const [secondAdmin, firstUser] = swap(users[0], admins[1]);
  logAdmin(secondAdmin);
  logUser(firstUser);
}

function test3() {
  console.log("test3:");
  const [secondUser, firstUser] = swap(users[0], users[1]);
  logUser(secondUser);
  logUser(firstUser);
}

function test4() {
  console.log("test4:");
  const [firstAdmin, secondAdmin] = swap(admins[1], admins[0]);
  logAdmin(firstAdmin);
  logAdmin(secondAdmin);
}

function test5() {
  console.log("test5:");
  const [stringValue, numericValue] = swap(123, "Hello World");
  console.log(` - String: ${stringValue}`);
  console.log(` - Numeric: ${numericValue}`);
}

[test1, test2, test3, test4, test5].forEach((test) => test());

前置知識

  • 泛型

思路

題目廢話很多, 直接忽略看報錯。

這個其實我在 你不知道的 TypeScript 泛型(萬字長文,建議收藏) 裡也講過了,直接看程式碼。

程式碼

export function swap<U, T>(v1: T, v2: U): [U, T] {
  return [v2, v1];
}

第八題

題目描述

Intro:

    Project grew and we ended up in a situation with
    some users starting to have more influence.
    Therefore, we decided to create a new person type
    called PowerUser which is supposed to combine
    everything User and Admin have.

Exercise:

    Define type PowerUser which should have all fields
    from both User and Admin (except for type),
    and also have type 'powerUser' without duplicating
    all the fields in the code.

題目大概意思是定義一個型別 PowerUser, 裡面包含 User 和 Admin 的所有屬性, 並且有一個欄位是固定的 type: 'powerUser'。

題目內建程式碼

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

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

type PowerUser = Omit<User & Admin, "type"> & { type: "powerUser" };

export type Person = User | Admin | PowerUser;

export const persons: Person[] = [
  {
    type: "user",
    name: "Max Mustermann",
    age: 25,
    occupation: "Chimney sweep",
  },
  { type: "admin", name: "Jane Doe", age: 32, role: "Administrator" },
  { type: "user", name: "Kate Müller", age: 23, occupation: "Astronaut" },
  { type: "admin", name: "Bruce Willis", age: 64, role: "World saver" },
  {
    type: "powerUser",
    name: "Nikki Stone",
    age: 45,
    role: "Moderator",
    occupation: "Cat groomer",
  },
];

function isAdmin(person: Person): person is Admin {
  return person.type === "admin";
}

function isUser(person: Person): person is User {
  return person.type === "user";
}

function isPowerUser(person: Person): person is PowerUser {
  return person.type === "powerUser";
}

export function logPerson(person: Person) {
  let additionalInformation: string = "";
  if (isAdmin(person)) {
    additionalInformation = person.role;
  }
  if (isUser(person)) {
    additionalInformation = person.occupation;
  }
  if (isPowerUser(person)) {
    additionalInformation = `${person.role}, ${person.occupation}`;
  }
  console.log(`${person.name}, ${person.age}, ${additionalInformation}`);
}

console.log("Admins:");
persons.filter(isAdmin).forEach(logPerson);

console.log();

console.log("Users:");
persons.filter(isUser).forEach(logPerson);

console.log();

console.log("Power users:");
persons.filter(isPowerUser).forEach(logPerson);

前置知識

  • 集合操作(交叉型別)
  • & 操作符
  • 泛型
  • Omit 泛型

思路

從題目資訊不難看出,就是讓我們實現 PowerUser。

有前面的分析不難得出我們只需要:

  • 合併 User 和 Admin 的屬性即可。 藉助 & 操作符可以實現。即 User & Admin
  • 增加特有的屬性 type: powerUser。 首先去掉上一步合併的 type 屬性, 然後繼續和 { type: "powerUser" } 交叉即可。
  • 增加 { type: "powerUser" } 之前使用內建泛型 Omit 將原本的 type 刪掉即可。

程式碼

type PowerUser = Omit<User & Admin, "type"> & { type: "powerUser" };

總結

以上就是給大家帶來的題目解析。 這八道題的考點有,按照我個人理解的重要程度劃分為:

  • type 和 interface 的基本操作(必須掌握)
  • 聯合型別 和 交叉型別(強烈建議掌握)
  • 型別斷言和型別收縮(強烈建議掌握)
  • 泛型和常見內建泛型(強烈建議掌握)
  • 函式過載(推薦掌握)

最後祝願大家告別 anyscript,成為 TypeScript 魔法師。

關注我

大家也可以關注我的公眾號《腦洞前端》獲取更多更新鮮的前端硬核文章,帶你認識你不知道的前端。

公眾號【 力扣加加
知乎專欄【 Lucifer - 知乎

點關注,不迷路!