目录

前端 10 问之 TypeScript (第一篇)

1、什么是 Typescript?

TypeScript 是 JavaScript 的一个超集,主要为 JavaScript 提供了类型系统,它由 Microsoft 开发,代码是开源的。

2、interface 和 type 的区别

相同点

  • 都可以描述一个对象或者函数

interface

1
2
3
4
5
6
7
8
interface User {
  name: string
  age: number
}

interface SetUser {
  (name: string, age: number): void;
}

type

1
2
3
4
5
6
type User = {
  name: string
  age: number
};

type SetUser = (name: string, age: number): void;

不同点

type 可以而 interface 不行

  • type 可以声明基本类型别名,联合类型
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 基本类型别名
type Name = string

// 联合类型
interface Dog {
    wong();
}
interface Cat {
    miao();
}

type Pet = Dog | Cat
  • type 语句中还可以使用 typeof 获取实例的 类型进行赋值
1
2
3
// 当你想获取一个变量的类型时,使用 typeof
let div = document.createElement("div");
type B = typeof div;

interface 可以而 type 不行

  • interface 能够声明合并
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface User {
  name: string
  age: number
}

interface User {
  sex: string
}

/*
User 接口为 {
  name: string
  age: number
  sex: string
}
*/

相关链接: typescript 中的 interface 和 type 到底有什么区别? TypeScript: Interfaces vs Types

3、never 关键词有什么用?

当有一个联合类型:

1
2
3
4
5
6
7
8
9
interface Foo {
  type: "foo";
}

interface Bar {
  type: "bar";
}

type All = Foo | Bar;

在 switch 当中判断 type,TS 是可以收窄类型的 (discriminated union):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function handleValue(val: All) {
  switch (val.type) {
    case "foo":
      // 这里 val 被收窄为 Foo
      break;
    case "bar":
      // val 在这里是 Bar
      break;
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val;
      break;
  }
}

注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:

1
type All = Foo | Bar | Baz;

然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。

参考链接: 尤雨溪:TypeScript 中的 never 类型具体有什么用?

4、is 关键词有什么用?

先看一串代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
function isString(test: any): boolean {
  return typeof test === "string";
}

function example(foo: any) {
  if (isString(foo)) {
    console.log("it is a string" + foo);
    console.log(foo.length);
    console.log(foo.hello());
  }
}
example("hello world");

上面的的代码不会有编译时错误,但是会有运行时错误。因为 字符串类型上没有 hello 方法。

这时候怎么优化,才能让 TS 在编译时就能检查到错误呢?

is 关键词可以派上用场,参考下面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
function isString(test: any): test is string{
  return typeof test === "string";
}

function example(foo: any){
  if(isString(foo)){
      console.log("it is a string" + foo);
      console.log(foo.length);
      console.log(foo.hello()); // ts Error: 类型“string”上不存在属性“hello”
  }
}

我们把 boolean 替换成 test is string,那么当函数 isString 返回 true 时, TS 会将 test 的类型从 any 收缩到 string,从而能精确的进行类型判断。

相关链接: What does the is keyword do in typescript?

5、infer 关键词有什么用?

infer 关键词常在条件类型中和 extends 关键词一同出现,表示将要推断的类型,作为类型变量可以在三元表达式的 True 部分引用。

例如,简单的类型提取:

1
2
3
4
type Unpacked<T> =  T extends (infer U)[] ? U : T;

type T0 = Unpacked<string[]>; // string
type T1 = Unpacked<string>; // string

下面将要提到的 ReturnType 工具类型 也是使用这种方式提取到了函数的返回类型:

1
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

相关链接: TypeScript 的 Infer 关键词

6、TypeScript 有哪些实用工具类型?

Partial<T>

将 T 中所有属性转换为可选属性,返回的类型可以是 T 的任意子集。​ 这在需要支持接受部分属性的场景下非常有用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface Todo {
  title: string;
  description: string;
  done: boolean;
}

function updateTodo(todo: Todo, newTodo: Partial<Todo>) {
  return { ...todo, ...newTodo };
}

const todo: Todo = {
  title: "First Todo",
  description: "this is the first todo",
  done: false,
};

updateTodo(todo, { done: true });

源码:

1
type Partial<T> = { [P in keyof T]?: T[P]; };

Required<T>

通过将 T 的所有属性设置为必选属性来构造一个新的类型,与 Partial 相对。

1
2
3
4
5
6
7
8
9
interface Example {
  a?: string;
  b?: string;
}

const example1: Example = { a: "aaa" }; // right

const example2: Required<Example> = { a: "aaa" };
// error: Property 'b' is missing in type '{ a: string; }' but required in type 'Required<Example>'

源码:

1
type Required<T> = { [P in keyof T]-?: T[P]; };

Readonly<T>

将 T 中所有属性设置为只读:

1
2
3
4
5
6
7
interface Todo {
  title: string;
}

const todo: Readonly<Todo> = { title: "First Todo" };

todo.title = "New Title"; // Cannot assign to 'title' because it is a read-only property.

源码:

1
type Readonly<T> = { readonly [P in keyof T]: T[P]; };

Pick<T,K>

通过在 T 中抽取一组属性 K 构建一个新类型:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
interface Todo {
  title: string;
  description: string;
  done: boolean;
}

type TodoBase = Pick<Todo, "title" | "done">;

const todo: TodoBase = {
  title: "First Todo",
  done: false,
};

源码:

1
2
// K是T的属性集合的子集
type Pick<T, K extends keyof T> = { [p in K]: T[p] };

ReturnType<T>

返回 function 的返回值类型:

1
2
3
4
5
type T0 = ReturnType<() => string>; // string

type T1 = ReturnType<() => Promise<number>>; // Promise<number>

type T2 = ReturnType<(s: string) => void>; // void

源码:

1
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

相关链接: TypeScript Utility Types 源码解析

7、如何处理才能在 TS 中引用 CSS 或者 图片使之不报错?

例如,在下面的 TS 文件中,直接导入 .scss 文件或者 .png文件,TS 的静态检查会出错:

1
2
import S from "./index.scss"; // TS Error: 找不到模块“./index.scss”或其相应的类型声明
import imgLogo from "./logo.png"; // // TS Error: 找不到模块“./logo.png”或其相应的类型声明

这时候需要编写 TS 申明文件,可以在项目根目录下添加一个 index.d.ts 文件,其中写入:

1
2
3
4
5
6
declare module '*.png';

declare module '*.module.scss' {
  const classes: { [key: string]: string };
  export default classes;
}

8、tsconfig.json 有哪些常用属性?

一般的 tsconfg.json 可以如下配置:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "moduleResolution": "node",
    "module": "esnext",
    "target": "es5",
    "noImplicitAny": false,
    "jsx": "react",
    "allowJs": true,
    "baseUrl": "./",
    "paths": {
      "@utils/*": ["src/utils/*"],
      "@containers/*": ["src/containers/*"],
      "@components/*": ["src/components/*"]
    }
  }
}

其中有一些常用的属性,例如:

1、allowJs: true

允许导入 .js 文件

2、paths

设置别名

3、allowSyntheticDefaultImports:true

1
2
3
4
// 这样不会报错
import React, { Component } from "react";
// 要不然只能
import * as React from "react";

4、esModuleInterop:true

 和 allowSyntheticDefaultImports:true 类似,但是比 allowSyntheticDefaultImports 更强大一些。

相关阅读:

esModuleInterop 到底做了什么?

5、moduleResolution:"node"

告诉 TypeScript 编译器使用哪种模块解析策略。若未指定,那么在使用了 –module AMD | System | ES2015 时的默认值为 Classic,其它情况时则为 Node

当指定为 node 时,会采用类似 NodeJS 的模块解析方式。那么写法上可以做如下优化:

1
2
3
4
//可以直接引用文件夹
import Head from "@components/head";
// 而不需要
import Head from "@components/head/index";

9、TS 的装饰器有没有用过?试着写一个用于计算函数执行时间的的装饰器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
/**
 * 装饰器方法 , 用于计算运行时间
 *
 * @param target 目标对象
 * @param propertyName 属性名
 * @param propertyDescriptor 属性描述器
 */
export function runtime(
  target: Object,
  propertyName: string,
  propertyDescriptor: PropertyDescriptor
): PropertyDescriptor {
  const method = propertyDescriptor.value;
  propertyDescriptor.value = function (...args) {
    console.time(propertyName);
    const result = method.apply(this, args);
    console.timeEnd(propertyName);
    return result;
  };
  return propertyDescriptor;
}

10、一道 LeetCode 招聘面试题

下面是一道 LeetCode 面试题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 假设有一个这样的类型:
interface initInterface {
  count: number;
  message: string;
  asyncMethod(input: Promise<string>): Promise<Action<number>>;
  syncMethod(action: Action<string>): Action<number>;
}

// 在经过 Connect 函数之后,返回值类型为

interface Result {
  asyncMethod(input: string): Action<number>;
  syncMethod(action: string): Action<number>;
}

// 其中 Action<T> 的定义为:
interface Action<T> {
  payload?: T
  type: string
}
// 现在要求写出Connect的函数类型定义。

答案:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type RemoveNonFunctionProps<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

type PickFunction<T> = Pick<T, RemoveNonFunctionProps<T>>;

type TransformMethod<T> = T extends (
  input: Promise<infer U>
) => Promise<Action<infer S>>
  ? (input: U) => Action<S>
  : T extends (action: Action<infer U>) => Action<infer S>
  ? (action: U) => Action<S>
  : never;

type ConnectAll<T> = {
  [K in keyof T]: TransformMethod<T[K]>;
};

type Connect<T> = ConnectAll<PickFunction<T>>;

参考链接: 不能不掌握的 ts 高级特性