[前端与TypeScript | 青训营笔记04]

91 阅读7分钟

前言

这是我参与 [第五届青训营] 伴学笔记创作活动的第 4 天,上回讲到Javascript,那这个Typescript是什么呢? 为什么要用Typescript呢?请听我娓娓道来!!!

为什么要用 TypeScript ?

JS 是动态类型 弱类型语言 TS 是静态类型 强类型语言

TypeScript 基础和语法

  1. 静态类型
  • 可读性增强:基于语法解析TSDoc,ide增强
  • 可维护性增强: 在编译阶段暴露大部分错误 => 多人合作的大型项目中,获得更好的稳定性和开发效率
  1. JS 的超集
  • 包含于兼容所有Js特性,支持共存
  • 支持渐进式引入与升级

对象类型

const bytedancer: IBytedancer = {
  jobId: 9303245,
  name: 'Lin',
  sex: 'man',
  age: 28,
  hobby: ' swimming'
}
interface IBytedancer {
/* 只读属性: 约束属性不可在对象初始化外赋值 */readonly jobid: number;
  name: string;
  sex: 'man' | 'woman' | 'other';
  age: number;
/* 可选属性: 定义该属性可以不存在 */
  hobby?: string;
/* 任意属性: 约束所有对象属性都必须是该属性的子类型 */
  [key: string]: any;
}

/* 报错: 无法分配到 “jobId"因为它是只读属性 */
bytedancer.jobId = 12345;
/* 成功: 任意属性标注下可以添加任意属性 */
bytedancer.plateform = 'data';/* 报错: 缺少属性“name",hobby可缺省 */
const bytedancer2: IBytedancer = {
  jobId: 89757,
  sex: 'woman',
  age: 18,
}

总结: 对象类型

interface I名字{ xx: 类型, [key:类型]: 类型 }

函数类型

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

const mult: (x: number,y: number) => number = (x,y) => x * y
interface IMult {
  (x:number, y:mumber): number';
}
    
const mult:IMult = (x,y) => x * y

总结: 函数类型 函数就是一个入参和出参的类型判断

interface I名字 { (x:类型):类型 }

函数重载

/* 对getDate函数进行重载,itimestamp为可缺省参数 */
function getDate(type: 'string', timestamp?: string): string; 
interface IGetDate {
  (type:'string', timestamp?: string): string;
  (type: (type: 'date', timestamp?: string): Date;
  (type: 'string''date', timestamp?: string): Date | string;
}
/* 不能将类型“(type: any,timestamp: any) => string  Date”分配给类型“IGetDate”
不能将类型“stringDate”分配给类型“string”。
不能将类型“Date”分配给类型“string”。ts(2322) */
const getDate2: IGetDate = (type,timestamp) => {
  const date = new Date(timestamp); 
  return type === 'string' ? date.toLocaleString() : date;
}

总结: 函数重载

interface I名字 {
(xx:类型,xx:类型):类型;
(xx:类型|类型,xx:类型):类型|类型;
}

数组类型

/* r类型 + 方括号] 表示 */
type IArr1 = number[];
/* 泛型表示 */
type IArr2 = Array<string | number | Record<string,number>>;
/* 元祖表示 */
type IArr3 = [number, number, string,string];
/* 接口表示 */
interface IArr4 {
  [key: number]: any;
}
const arr1: 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,{},[]];

总结: 数组类型

  1. 全是数字的数组 (推荐)

type I名字= number[]

  1. 泛型 (推荐)

type I名字 = Array<类型 | 类型| Record<类型,类型>>

  1. 接口表示

interface I名字 {
[xx: 类型]: any类型;
}

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补充类型 暂时理解不了
理解不了的原因是: 1. 英文不认识 2. 文档没查过 3. 代码没见过

Typescript泛型

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).filltarget);
/* 报错: 类型“number”的参数不能赋给类型“string”的参数 */
getStrArr(123);

/* 泛型参数默认类型 */
type IGetRepeatArr<T = number> = (target: T) => T[];
const getRepeatArr: IGetRepeatArr = target => new Array(100).fill(target);
/* 报错: 类型“string”的参数不能赋给类型“number”的参数 */
getRepeatArr('123')

总结: 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>;
}

字符串/数字 字面量

/* 允许指定字符串/数字必须的固定值 *

/* IDomTag必须为html、body、div、span中的其一 */
type IDomTag ='html' | 'body' | 'div' | 'span';
/* IOddNumber必须为1、3、5、7、9中的其一 */
type IOddNumber = 1 | 3 | 5 | 7 | 9;

Typescript 高级类型

case | solution | evolution

联合/交叉类型

为书籍列表编写类型 =>

const bookList = [{
  author:'xiaoming',
  type: 'history',
  range:'2001-2021',
},{
  author: 'xiaoli',
  type:'story',
  theme:'love',
}]

类型声明繁琐,存在较多重复

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;多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性
type IBookList = Array<{
  author: string;
} & ({
  type:'history';
  range: string;
} | {
  type: 'story';
  theme: string;
})>

总结: 联合/交叉类型 通过() & | 来去根据语义进行定义类型,其中类型用{}去包裹

类型保护与类型守卫

interface IA { a: 1,a1: 2 }
interface IB { b: 1,b1: 2 }
function log(arg: IA | IB) {
/* 报错: 类型“IAlIB”上不存在属性“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);
  }
}

上面是通过 is 来去确定是 IA ,通过as IA 来去让Typescript 识别就是IA 总体比较麻烦

以下就是Typescript 智能的推断

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

高级类型

/**
实现merge函数类型
* 要求sourceObj必须为targetObj的子集
*/
function merge1(sourceObj, targetObj) {
  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 }
};

interface ISourceObj {
  x?: string;
  y?: string;
}
interface ITargetObj {
  x: string;
  y: string
}
type IMerge = (sourceObj: ISourceObj, targetObj: ITargetObj) =>
ITargetObj;
/** 
 * 类型实现繁琐: 若obj类型较为复杂,则声明source和target便需要大量重复2遍
 * 容易出错: 若target增加/减少key,则需要source联动去除
*/
interface IMerge { 
  <T extends Record<string, any>>(sourceObj: IPartial<T>, targetObj: T): T;
}
type IPartial<T extends Record<string, any> > = {
  [P in keyof T]?: T[P];
}
// 索引类型: 关键字(keyof],其相当于取值对象中的所有key组成的字符串字面量,如
type lKeys = keyof { a: string; b: number }; // => type lKeys = "a""b"

// 关键字【in】,其相当于取值 字符串字面量 中的一种可能,配合泛型P,即表示每个key

// 关键字【?】,通过设定对象可选选项,即可自动推导出子集类型

函数返回值类型

// 实现函数delayCall的类型声明
// delayCall接受一个函数作为入参,其实现延迟1s运行函数
// 其返回promise,结果为入参函数的返回结果
function delayCall(func) {
  return new Promise(resolve => { 
  setTimeout(() =>{
    const result = func();
    resolve(result);
   },1000);
  })
}
type IDelayCall = <T extends () => any>(func: T) => IReturnType<T>;
type IReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
// 关键字(extends] 跟随泛型出现时表示类型推断,其表达可类比三元表达式
// 如 T=== 判断类型 ? 类型A : 类型B

// 关键字[infer] 出现在类型推荐中,表示定义类型变量,可以用于指代类型
// 如 该场景下,将函数的返回值类型作为变量,使用新泛型R表示,使用在类型推荐命中的结果中

总结: 函数返回值类型

  1. 先找分解符=
  2. 判断等式的两边,那边是声明,那边是推断类型
  3. 找<> 进行判断层数里的类型声明或者推断
  4. 找到关键字和符号 列如:extends infer X ? : any
  5. 再进行整体类型推断

Typescript工程应用 -Web

webpack

  1. 配置webapack loader相关配置
  2. 配置tsconfg.js文件
  3. 运行webpack启动 / 打包
  4. loader处理ts文件时,会进行编译与类型检查

babel 相关loader:

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

Typescript工程应用 -Node

  1. 使用TSC 编译 code.ts -> tsc[code.ts] -> code.js

  2. 安装Node与npm

  3. 配置tsconfig.js文件

  4. 使用tsc 运行编译得到js文件