写给小白的「Typescript 入门+进阶+疑难点总结」

135 阅读10分钟
  • 持续更新
  • 24.06.22
  • 24.09.02

基础学习

掘金各大总结,随便搜搜包你满意

「1.9W字总结」一份通俗易懂的 TS 教程,入门 + 实战!

混淆概念

type VS interface

在 Typescript 里,这俩概念很容易混淆,因为都可以用来表示接口

相同点

  • 都可以定义对象或函数
interface P1 {
    name: string,
    age: number,
    getAge: (name:string) => number 
}

type P2 = {
    name: string,
    age: number,
    getAge: (name:string) => number
}
  • 都可以相互合并继承
    type exampleType1 = {
        name: string
    }
    interface exampleInterface1 {
        name: string
    }
    
    
    type exampleType2 = exampleType1 & {
        age: number
    }
    type exampleType2 = exampleInterface1 & {
        age: number
    }
    interface exampleInterface2 extends exampleType1 {
        age: number
    }
    interface exampleInterface2 extends exampleInterface1 {
        age: number
    }

不同点

  • type可以定义 基本类型的别名,如 type myType = string

  • type可以通过 typeof 操作符来定义,如 type myType = typeof someObj

  • type可以申明 联合类型,如 type myType = myType1 | myType2

  • type可以申明 元组类型,如 type myType = [myType1, myType2]

  • interface可以 声明合并,即支持多次声明,最后的interface会合并所有声明。type多次声明会报错。

选用时机

  • 在定义公共 API(如编辑一个库)时使用 interface,这样可以方便使用者继承接口;

  • 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强;

  • type 类型不能二次编辑,而 interface 可以随时扩展。

any VS unknow

  • 不清楚用什么类型,可以使用 any 类型。这些值可能来自于动态的内容。对 any 类型的指执行任何操作,都不需要事先的检查
let value: any;

value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0][1]; // OK

但这会导致一个问题,有可能你传入的值根本没有相应方法,但由于它是 any 类型,在运行时才会报错

  • unknown 也可以赋值所有属性,不同的是,unknown 上不能执行操作
let value: unknown;

value = true; // OK
value = 42; // OK 
value = "Hello World"; // OK 
value = []; // OK 
value = {}; // OK

value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0][1]; // Error

  • unknown 类型只能被赋值给 any 类型和 unknown 类型本身,而 any 类型可以赋值给所有类型
let anyVal: any;
let unknownVal: unknown;
let strVal: string;

strVal = anyVal; // OK
strVal = unknownVal; // Error

疑难理解

断言

使用断言有两种格式

<类型>值

// 或者

值 as 类型

推荐以 as 方式,因为 JSX 这样的语法中只支持 as 方式

  • 我们来看一段代码
function getLength(arg: number | string): number {
    if(arg.length){
        return arg.length
    } else {
        return arg.toString().length
    }
}

这时候编译器会报错,因为当arg为number时,arg.length并不存在,虽然代码已经写明了,有这个方法的时候才执行
这时候就需要用到断言了,它就是告诉编译器 我(开发者)比你(编译器)更清楚这个参数是什么类型,你就别给我报错了”

  • as 断言
function getLength(arg: number | string): number {
    if((arg as string).length){
        return (arg as string).length
    } else {
        return arg.toString().length
    }
}

// 简写
function getLength(arg: number | string): number {
    const str = arg as string;
    if(str.length){
        return (arg as string).length
    } else {
        const number = arg as number;
        return number.toString().length
    }
}
  • <> 断言
function getLength(arg: number | string): number {
    if ((<string>arg).length) {
      return (<string>arg).length
    } else {
      return arg.toString().length
    }
}

type & typeof

在下面的例子中,typeof tom 虽然是 "object",但 type Tom = typeof tom并不等同于type Tom2 = object

interface Person {
  name: string;
  age: number;
}

const tom: Person = { name: 'tom', age: 30 };
console.log(typeof tom); // -> object

type Tom = typeof tom; // -> Tom 是 Person 类型
type Tom2 = object; // -> Tom2 是 object 类型

const obj : Tom = {name: 's', age: 30} // 正确
// const obj : Tom = {name: 's'} // 报错
const obj2 : Tom2 = {name: 's'} // 正确

keyof

keyof 操作符可以用来一个对象中的所有 key 值:

interface Person {
    name: string;
    age: number;
}

type K1 = keyof Person; // "name" | "age"
type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" 
type K3 = keyof { [x: string]: Person };  // string | number


// const k1:K1 = "age"; //正确
const k1:K1 = "name"; //正确
const k2:K2 = "toString" //正确
const k3:K3 = "string" //正确

映射类型

Typescript 允许将一个类型映射成另外一个类型。也就是说,其实最终都是定义 type

  • in 实现对联合类型的遍历,例如:
type Person = "name" | "school" | "major"

type Obj =  {
  [p in Person]: string
}
  • Partial Partial<T> T的所有属性映射为可选的,例如:
interface IPerson {
    name: string
    age: number
}

type IPartial = Partial<IPerson>

let p1: IPartial = {name: "only name"}
  • Readonly Readonly<T>T的所有属性映射为只读的
  • Pick Pick用于抽取对象子集,挑选一组属性并组成一个新的类型,例如:
interface IPerson {
  name: string
  age: number
  sex: string
}

type IPick = Pick<IPerson, 'name' | 'age'>

let p1: IPick = {
  name: 'cc',
  age: 18
}

  • Record Record<U,T>将创建新属性的非同态映射类型。不好理解?直接看例子
interface IPerson {
  name: string
  age: number
}

type IRecord = Record<string, IPerson>

let personMap: IRecord = {
   person1: {
       name: 'cc',
       age: 18
   },
   person2: {
       name: 'vv',
       age: 25
   } 
}

类型保护

A type guard is some expression that performs a runtime check that guarantees the type in some scope. —— TypeScript 官方文档 好吧,管它是什么,看例子就对了。
注意与断言的区分!!!

  • typeof

typeof 用于判断 numberstringboolean或 symbol 四种类型

// 错误写法
function getLength(arg: number | string): number {
    if(arg.length){ // 报错
        return arg.length
    } else {
        return arg.toString().length
    }
}
// 正确写法
function getLength(arg: number | string): number {
    if(typeof arg === "string"){
        return arg.length
    } else {
        return arg.toString().length
    }
}
  • in

in 用于判断一个属性/方法是否属于某个对象

// 错误写法
interface Admin {
  name: string;
  privileges: string[];
}

interface Employee {
  name: string;
  startDate: Date;
}


function printEmployeeInformation(emp: Employee | Admin) {
  if (emp.privileges) { // 报错
    return emp.privileges;
  }
  if (emp.privileges) {
    return emp.startDate;
  }
  return ""
}
// 正确写法
function printEmployeeInformation(emp: Employee | Admin) {
  if ("privileges" in emp) {
    return emp.privileges;
  }
  if ("startDate" in emp) {
    return emp.startDate;
  }
  return ""
}
  • instanceof

instanceof 用于判断一个实例是否属于某个类

interface Padder {
  getPaddingString(): string;
}

class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}

class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}

let padder: Padder = new SpaceRepeatingPadder(6);

if (padder instanceof SpaceRepeatingPadder) {
  // padder的类型收窄为 'SpaceRepeatingPadder'
}

泛型约束

默认情况下,泛型函数的类型变量 Type 可以代表多个类型,这导致无法访问任何属性

function id<Type>(value: Type): Type {
  console.log(value.length) // error
  return value
}

这是 Type 可以代表任意类型,无法保证一定存在 length 属性,比如 number 类型就没有 length 此时,就需要为泛型添加约束来收缩类型(缩窄类型取值范围)

添加泛型约束收缩类型,主要有以下两种方式:

  • 指定更加具体的类型
  • 添加约束
  1. 指定更加具体的类型
    可以将类型修改为 Type[](Type 类型的数组),因为只要是数组就一定存在 length 属性(不推荐这种做法)
function id<Type>(value: Type[]): Type[] {
  console.log(value.length)
  return value
}
  1. 添加约束
// 创建一个接口
interface ILength { length: number }

// Type extends ILength 添加泛型约束
// 解释:表示传入的 类型 必须满足 ILength 接口的要求才行,也就是得有一个 number 类型的 length 属性
function id<Type extends ILength>(value: Type): Type {
  console.log(value.length)
  return value
}

首先创建描述约束的接口 ILength,该接口要求提供 length 属性。通过 extends 关键字使用该接口,为泛型(类型变量)添加约束。
该约束表示:传入的类型必须具有 length 属性

这时候,只要传入的实参(比如,数组)只要有 length 属性即可(类型兼容性)

多个泛型类型变量

泛型的类型变量可以有多个,并且类型变量之间还可以约束

下面是创建一个函数来获取对象中属性的值:

function getProp<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key]
}
let person = { name: 'jack', age: 18 }
getProp(person, 'name')

这里有两个泛型类型,TypeKey,用逗号分隔。

keyof 关键字接收一个对象类型,生成其键名称(可能是字符串或数字)的联合类型。类型变量 Key 受 Type 约束,可以理解为:Key 只能是 Type 所有键中的任意一个,或者说只能访问对象中存在的属性

// Type extends object 表示: Type 应该是一个对象类型,如果不是 对象 类型,就会报错
// 如果要用到 对象 类型,应该用 object ,而不是 Object
function getProperty<Type extends object, Key extends keyof Type>(obj: Type, key: Key) {
 return obj[key]
}

泛型接口

接口也可以配合泛型来使用,以增加其灵活性,增强其复用性

interface IdFunc<Type> {
  id: (value: Type) => Type
  ids: () => Type[]
}

在接口名称的后面添加 <类型变量>,那么,这个接口就变成了泛型接口
接口的类型变量,对接口中所有其他成员可见,他们都可以使用类型变量

使用泛型接口时,需要显式指定具体的类型

let obj: IdFunc<number> = {
  id(value) { return value },
  ids() { return [1, 3, 5] }
}

常用技巧

  1. 联合类型通常与 null 或 undefined 一起使用

  2. tsconfig.json作用

用于标识 TypeScript 项目的根路径;
用于配置 TypeScript 编译器;
用于指定编译的文件

  1. 只要 tsconfig.json 中的配置包含了 typing.d.ts 文件,那么其他所有 *.ts 文件就都可以获得声明文件中的类型定义

  2. 在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。具体而言,x! 将从 x 值域中排除 null 和 undefined 。

let mayNullOrUndefinedOrString: null | undefined | string;
mayNullOrUndefinedOrString!.toString(); // ok
mayNullOrUndefinedOrString.toString(); // 报错
  1. import引入类型声明需要type关键字,因为 import type 是用来导入类型声明的语法,它不会在编译后生成实际的代码,而是只在编译期间进行类型检查。而 import 则是用来导入具体的值或模块的语法,会在编译后生成实际的代码。因此,如果只需要使用类型声明而不需要实际的值或模块,应该使用 import type
import type { OwnType } from '@/type';

TS怎么给引入的第三方库设置类型声明文件

目前,几乎所有常用的第三方库都有相应的类型声明文件,第三方库的类型声明文件有两种存在形式:

1、 库自带类型声明文件(如axios)
这种情况下,正常导入该库,TS 就会自动加载库自己的类型声明文件,以提供该库的类型声明

2、 由 TS官方写
DefinitelyTyped 是一个TS官方提供的GIT仓,用来提供高质量 TypeScript 类型声明

可以通过 npm/yarn 来下载该仓库提供的 TS 类型声明包,这些包的名称格式为:@types/*,比如,@types/react、@types/lodash

3、基于库自带类型声明再自定义声明 假设原有的 axios 声明不满足当前条件,我们可以在 index.d.ts 下自定义部分声明,该值会与原有声明文件中的声明合并生效

declare module 'axios' {
  interface AxiosRequestConfig {
    mask?: boolean; // 自定义属性
    repeat?: boolean; 
  }
}

在实际项目开发时,如果你使用的第三方库没有自带的声明文件,VSCode 会给出明确的提示

为什么会觉得难用

在实际使用中,吐槽Typescript的越来越多(我也在吐槽,我也是新手T.T)。这里简单分析一下

类型复用

这里主要有类型未复用问题和类型复用不够的问题。

  1. 前者的话,type通过交叉类型&复用,interface通过继承extends,上面已经讲过了

  2. 后者场景是,一般的复用想到的都是新增属性,但如果某个interface已经大部分满足条件,唯有一个属性不满足,这时候很多人就不会复用了。

例如,有一个已有的类型A需要复用,但其中的属性c需要变成属性e

interface A {
  a: string;
  b: string;
  c: string;
}

我们可以利用TypeScript提供的工具类型Omit来更高效地实现这种复用。

interface A {
  a: string;
  b: string;
  c: string;
}

// 排除某些属性
interface B extends Omit<A, 'c'> {
  e: string;
}

// 选择某些属性
interface B extends Pick<A, 'a' | 'b'> {
  e: string;
}

处理含有不同元素的数组

有时候,一个数组可能包含不同类型,这时候可以使用元组

参数数量和类型不确定

一般这种情况,很多人选择使用any。但其实我们可以使用函数重载,针对不同参数定义多个重载函数(虽然更麻烦,毕竟多个函数)。