青训营 |「TypeScript入门」笔记

发表于 2022-01-28 3824 字 20 min read

这节课老师讲了 TypeScript 的用处与基本语法、高级类型的应用、类型保护与类型守卫

什么是 TypeScript

发展历史

  • 2012-10:微软发布了 TypeScript 第一个版本(0.8)
  • 2014-10:Angular 发布了基于 TypeScript 的 2.0 版本
  • 2015-04:微软发布了 Visual Studio Code
  • 2016-05:@ ty pes/react 发布,TypeScript 可开发 React
  • 2020-09:Vue 发布了 3.0 版本,官方支持 TypeScript
  • 2021-11:v4.5 版本发布

为什么是 TypeScript

image.png

动态类型执行过程中进行类型的匹配,js 的弱类型会在执行时进行隐式类型转换,而在静态类型中则不然

TypeScript 则为静态类型:java、c/c++等

  • 可读性增强:基于语法解析 TSDoc,ide 增强
  • 可维护性增强:在编译阶段暴露大部分错误
  • 多人合作的大型项目中,可以获得更好的稳定性和开发效率

TypeScript 是JS 的超集

  • 包含于兼容所有 Js 特性, 支持共存
  • 支持渐进式引入与升级

基本语法

基本数据类型

js ==> ts

image.png

可以看到,ts 的类型定义方式:let 变量名: 类型 = 值;

TypeScript 基础类型

对象类型

接口 · TypeScript 中文网

// 创建一个对象,包括以下属性,类型为IBytedancer
// I表示自定义的一个类型(一个命名约定),与类和对象进行区分
const bytedancer: IBytedancer = {
  jobId: 9303245,
  name: 'Lin',
  sex: 'man',
  age: 28,
  hobby: 'swimming',
};
// 定义一个类型为IBytedancer
interface IBytedancer {
  /* 只读属性readonly:约束属性不可在对象初始化外赋值 */
  readonly jobId: number;
  name: string;
  sex: 'man' | 'woman' | 'other';
  age: number;
  /* 可选属性:定义该属性可以不存在 */
  hobby?: string;
  /* 任意属性:约束所有对象属性都必须是该属性的子类型 */
  [key: string]: any; // any 任何类型
}
/* 报错:无法分配到"jobId",因为它是只读属性 */
bytedancer.jobId = 12345;
/* 成功:任意属性标注下可以添加任意属性 */
bytedancer.plateform = 'data';
/* 报错:缺少属性"name", 而hobby可缺省 */
const bytedancer2: IBytedancer = {
  jobId: 89757,
  sex: 'woman',
  age: 18,
};

函数类型

js:

function add(x, y!) {
 return x + y;
}
const mult = (x, y) =>  x * y;

ts:函数 · TypeScript 中文网

function add(x: number, y: number): number {
  return x + y;
}
const mult: (x: number, y: number) => number = (x, y) => x * y;
// 简化写法,定义接口IMult
interface IMult {
  (x: number, y: number): number;
}
const mult: IMult = (x, y) => x * y;

可以看到,格式为function 函数名(参数:类型...):返回值类型

函数重载

/* 对getDate函数进行重载,timestamp为可缺省参数 */
function getDate(type: 'string', timestamp?: string): string;
function getDate(type: 'date', timestamp?: string): Date;
function getDate(type: 'string' | 'date', timestamp?: string): Date | string {
  const date = new Date(timestamp);
  return type === 'string' ? date.toLocaleString() : date;
}
const x = getDate('date'); // x: Date
const y = getDate('string', '2018-01-10'); // y: string

简化形式如下:

interface IGetDate {
  (type: 'string', timestamp?: string): string; // 这个地方返回类型改为any就可以通过了
  (type: 'date', timestamp?: string): Date;
  (type: 'string' | 'date', timestamp?: string): Date | string;
}
/* 报错:不能将类型"(type: any, timestamp: any) => string | Date"分配给类型"IGetDate"。
 不能将类型"string | Date" 分配给类型"string"。
 不能将类型 "Date"分配给类型"string"。ts(2322) */
const getDate2: IGetDate = (type, timestamp) => {
  const date = new Date(timestamp);
  return type === 'string' ? date.toLocaleString() : date;
};

数组类型

type作用就是给类型起一个新名字,相当于 c++中的 typedef

/* 「类型+方括号」表示 */
type IArr1 = number[];
/* 泛型表示 这两种最常用*/
type IArr2 = Array<string | number | Record<string, number>>;
/* 元组表示 */
type IArr3 = [number, number, string, string];
/* 接口表示 */
interface IArr4 {
  [key: number]: any;
}

const arrl: IArr1 = [1, 2, 3, 4, 5, 6];
const arr2: IArr2 = [1, 2, '3', '4', { a: 1 }];
const arr3: IArr3 = [1, 2, '3', '4'];
const arr4: IArr4 = ['string', () => null, {}, []];

TypeScript 补充类型

  • 空类型:表示无赋值
  • 任意类型:是所有类型的子类型
  • 枚举类型:支持枚举值到枚举名的正、反向映射
/* 空类型,表示无赋值 */
type IEmptyFunction = () => void;
/* 任意类型,是所有类型的子类型 */
type IAnyType = any;
/* 枚举类型:支持枚举值到枚举名的正、反向映射 */
enum EnumExample {
  add = '+',
  mult = '*',
}
EnumExample['add'] === '+';
EnumExample['+'] === 'add';
enum ECorlor {
  Mon,
  Tue,
  Wed,
  Thu,
  Fri,
  Sat,
  Sun,
}
ECorlor['Mon'] === 0;
ECorlor[0] === 'Mon';
/*泛型*/
type INumArr = Array<number>;

Typescript 泛型

泛型,之前学过 c++的话 dddd,跟 c++中的差不多:不预先指定具体的类型,而在使用的时候再指定类型的一种特性

function getRepeatArr(target) {
 return new Array(100).fill(target);
}
type IGetRepeatArr = (target: any) => any[];
/* 不预先指定具体的类型,而在使用的时候再指定类型的一种特性 */
type IGetRepeatArrR = <T>(target: T) => T[];

泛型还可以使用在以下场景中:

/*泛型接口&多泛型*/
interface IX<T, U> {
  key: T;
  val: U;
}
/* 泛型类 */
class IMan<T> {
  instance: T;
}
/* 泛型别名 */
type ITypeArr<T> = Array<T>;

泛型还可以进行约束范围

/* 泛型约束:限制泛型必须符合字符串 */
type IGetRepeatStringArr = <T extends string>(target: T) => T[];
const getStrArr: IGetRepeatStringArr = (target) => new Array(100).fill(target);
/* 报错:类型"number"的参数不能赋给类型“string"的参数 */
getStrArr(123);

/* 泛型参数默认类型 */
type IGetRepeatArr<T = number> = (target: T) => T[]; // 与结构中的默认赋值有点类似
const getRepeatArr: IGetRepeatArr = (target) => new Array(100).fill(target); // 这里的IGetRepeatArr就是一个类型别名,此处没有传参数给这个类型别名
/* 报错:类型"string"的参数不能赋给类型“numbe r"的参数 */
getRepeatArr('123');

类型别名 & 类型断言

类型断言

有时候你会遇到这样的情况,你会比 TypeScript 更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。

通过类型断言 这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript 会假设你,程序员,已经进行了必须的检查。

let someValue: any = 'this is a string';

let strLength: number = (someValue as string).length;

基础类型 · TypeScript 中文网

/*通过type关键字定义了IObjArr的别名类型*/
type IObjArr = Array<{
 key: string;
 [objKey: string]: any;
}>
function keyBy<T extends IObjArr>(objArr: Array<T>) {
 /* 未指定类型时,result类型为{} */
 const result = objArr.reduce((res, val, key) => {
  res[key] = val;
  return res;
 }, {});
    /* 通过as关键字,断言result类型为正确类型 */
    return result as Record<string, T> ;
}

上述代码,中有几个点需注意:

reduce() 函数对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

语法:arr.reduce(callback(accumulator, currentValue[, index[, array]])[, initialValue])

字符串/数字 字面量

/* 允许指定字符 串/数字必须的固定值*/
/* IDomTag必须为html、body、div、 span中的其一*/
type IDomTag = 'html' | ' body' | 'div' | 'span';
/* IOddNumber必须为1、 3、5、7、9中的其一 */
type IOddNumber = 1 | 3 | 5 | 7 | 9;

高级类型

联合/交叉类型

为书籍列表编写类型 -> ts 类型声明繁琐存在较多重复。高级类型

const bookList = [
  {
    // 普通js
    author: 'xiaoming',
    type: 'history',
    range: '2001 -2021',
  },
  {
    author: 'xiaoli',
    type: 'Story',
    theme: 'love',
  },
];
// ts 繁琐
interface IHistoryBook {
  author: String;
  type: String;
  range: String;
}
interface IStoryBook {
  author: String;
  type: String;
  theme: String;
}
type IBookList = Array<IHistoryBook | IStoryBook>;
  • 联合类型: IA | IB; 联合类型表示一个值可以是几种类型之一
  • 交叉类型: IA & IB; 多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性

上述代码可以通过 ts 简化为:

type IBookList = Array<
  {
    author: string;
  } & (
    | {
        type: 'history';
        range: string;
      }
    | {
        type: 'story';
        theme: string;
      }
  )
>;
/* 限制了author只能为string类型,而type只能'history'/'story'二选一,并且type不同可能的属性不同 */

类型保护与类型守卫

  • 访问联合类型时,处于程序安全,仅能访问联合类型中的交集部分
interface IA {
  a: 1;
  a1: 2;
}
interface IB {
  b: 1;
  b1: 2;
}
function log(arg: IA | IB) {
  /*报错:类型"IA | IB" 上不存在属性"a”。 类型"IB"上不存在属性"a"
    结论:访问联合类型时,处于程序安全,仅能访问联合类型中的交集部分*/

  if (arg.a) {
    console.log(arg.a1);
  } else {
    console.log(arg.b1);
  }
}

上述报错可通过类型守卫解决:定义一个函数,其返回值是一个类型谓词生效范围为子作用域

interface IA {
  a: 1;
  a1: 2;
}
interface IB {
  b: 1;
  b1: 2;
}

/*类型守卫:定义一个函数,。它的返回值是一个类型谓词,生效范围为子作用域 */
function getIsIA(arg: IA | IB): arg is IA {
  return !!(arg as IA).a;
}
function log2(arg: IA | IB) {
  /* 不存在报错了 */
  if (getIsIA(arg)) {
    console.log(arg.a1);
  } else {
    console.log(arg.b1);
  }
}

或者 typeof 和 instance 判断

// 实现函数reverse 可将数组或字符串进行反转
function reverse(target: string | Array<any>) {
  /* typof 类型保护*/
  if (typeof target === 'string') {
    return target.split('').reverse().join('');
  }
  /* instance 类型保护*/
  if (target instanceof Object) {
    return target.reverse();
  }
}

不会每次都这么麻烦吧,事实上,只有当两个类型没有任何重合点的话才需要类型守卫,如上述的书本例子,可以进行自动类型推断。

// 实现函数logBook类型
// 函数接受书本类型,并logger出相关特征
function logBook(book: IBookItem) {
 // 联合类型+类型保护=自动类型推断
 if (book.type === 'history'){
  console.log(book.range)
    } else{
        console.log book.theme);
    }
}

再来看一个 case,实现一个子集不污染的合并函数 merge,将 sourceObj 合并到 targetObj 中,sourceObj 必须为 targetObj 的子集

function merge1(sourceObj, targetObj) {
  // js中,实现复杂,这样才能不污染
  const result = { ...sourceObj };
  for (let key in targetObj) {
    const itemVal = sourceObj[key];
    itemVal && (result[key] = itemVal);
  }
  return result;
}
function merge2(sourceObj, targetObj) {
  // 若这两个入参的类型没问题,则可以这样
  return { ...sourceObj, ...targetObj };
}

而一种简单的思想就是在 ts 中编写两个类型,进行判断,但这样又会存在实现繁琐,增加 target 需要 source 联动去除,重复维护了两份 x、y

interface ISource0bj {
  x?: string;
  y?: string;
}
interface ITarget0bf {
  x: string;
  y: string;
}
type IMerge = (source0bj: ISource0bj, target0bj: ITarget0bj) => ITargetObj;
/* 类型实现繁琐:若obj类型较为复杂,则声明source和target便需要大量重复2遍
容易出错:若target增加/减少key,则需要source联动去除 */

通过泛型,改进,这里涉及到几个个知识点

  • Partial:一个常见的任务是将一个已知的类型每个属性都变为可选的

TypeScript 提供了从旧类型中创建新类型的一种方式——映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 (直接写就行,ts 内置了)

  • 关键字keyof,其相当于取值对象中的所有 key 组成的字符串字面量
  • 关键字in,其相当于取值字符串字面量中的一种可能,配合泛型 P, 即表示每个 key
  • 关键字 ? ,通过设定对象可选选项,即可自动推导出子集类型
interface IMerge {
  <T extends Record<string, any>>(sourceObj: Partial<T>, targetObj: T): T;
}
// Partial内部实现
type IPartial<T extends Record<string, any>> = {
  [P in keyof T]?: T[P];
};
// 索引类型:关键字[keyof] ,其相当于取值对象中的所有key组成的字符串字面量,如
type IKeys = keyof { a: string; b: number }; // => type IKeys ="a" | "b"
// 关键字[in],其相当于取值 字符串字面量中的一种可能,配合泛型P, 即表示每个key
// 关键字[ ? ],通过设定对象 可选选项,即可自动推导出子集类型

函数返回值类型

函数返回值类型在定义时候是不明确的,也应该通过泛型进行表达

下文代码 delayCall 接受一个函数作为入参,其实现延迟 1s 运行函数 func,其返回 promise,结果为入参函数的返回结果

// 如何实现函数delayCall的类型声明
// delayCall接受一个函数作为入参,其实现延迟1s运行函数
// 其返回promise,结果为入参函数的返回结果
function delayCall(func) {
    return new Promisd(resolve => {
        setTimeout(() => {
            const result= func );
            resolve(result);
        },1000);
    });
}
  • 关键字 extends 跟随泛型出现时,表示类型推断,其表达可类比三元表达式

    • T === 判断类型?类型A:类型B -> T extends 判断类型?类型A:类型B
  • 关键字 infer 出现在类型推荐中,表示定义类型变量,可以用于指代类型

    infer 简单示例如下:

    type ParamType<T> = T extends (...args: infer P) => any ? P : T;

    在这个条件语句 T extends (...args: infer P) => any ? P : T 中,infer P 表示待推断的函数参数。

    整句表示为:如果 T 能赋值给 (...args: infer P) => any,则结果是 (...args: infer P) => any 类型中的参数 P,否则返回为 T

    • 在这里就相当于把这个函数返回值类型指代为 R
type IDelayCall = <T extends () => any>(func: T) => ReturnType<T>;
type IReturnType<T extends (...args: any) => any> = T extends (...args: any) => inferR ? R : any;

// 关键字[extends] 跟随泛型出现时,表示类型推断,其表达可类比三元表达式
// 如T === 判断类型?类型A:类型B
// 关键字[infer] 出现在类型推荐中,表示定义类型变量,可以用于指代类型
// 如该场景下,将函数的返回值类型作为变量,使用新泛型R表示,使用在类型推荐命中的结果中

工程应用

TypeScript 工程应用——Web

  1. 配置 webapack loader相关配置
  2. 配置 tsconfig.js文件(宽松——严格,都可以定义)
  3. 运行 webpack启动/ 打包
  4. loader 处理 ts 文件时, 会进行编译与类型检查

相关 loader:

  1. awesome-typescript-loader
  2. or babel-loader

TypeScript 工程应用——Node

使用 TSC 编译

  1. 安装 Node 与 npm
  2. 配置 tsconfig.js 文件
  3. 使用 npm 安装 tsc
  4. 使用 tsc 运行编译得到 js 文件

image.png

总结感想

这节课老师讲了 TypeScript 的用处与基本语法、和 JS 的对比、高级类型的应用,后续也深入讲了一下类型保护与类型守卫,在最后总结了 TypeScript 如何在工程中进行应用。TypeScript 作为 JS 的一个超集,他增加了类型检查的功能,可以在编译阶段就将代码中的错误暴露出来,这是 js 这类动态类型所不具备的,在多人合作的大型项目中,使用 TS 往往可以获得更好的稳定性和开发效率。

本文引用的大部分内容来自林皇老师的课以及 ts 官方文档~