TypeScript 練習題

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

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

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

系列安排:github

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

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

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

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

<!-- more -->緩存

前言

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

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

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

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

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

爲了避免讓文章太過於冗長, 本篇文章分兩次發佈, 一次 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 - 知乎

點關注,不迷路!

相關文章
相關標籤/搜索