typescript 打好基础,走起....

430 阅读15分钟

1625735171551.jpg typescript :一个基于Structural Type System(结构类型系统)的静态类型检查器,相对于javascript而言,在你运行代码前,捕捉到错误并修复。

从typscript 的定义得出2个关键点:

  • 结构类型:一个对象从类型角度的去描述其结构,侧重的是类型结构, 如:一个数组的里面每一个元素是什么类型
  • 预期目标:在代码运行前,就捕捉到错误,尽可能早的帮你发现错误
function print(name: string, age: number) {
  console.log(name,age)
}

从类型,结构去分析函数 print的第一个参数name,必须是一个string 字符串,第二个参数age,必须是一个number数字,这2个组在一起就描述了函数的参数类型结构。

一个是类型,一个是结构,是typescript 进行静态类型检查的基础。

Basic Type 基本类型

typescript基本类型主是一些基本的数据类型,它是组合成复杂类型的基本单位。就像学习文字一样,他是笔画。

1.布尔值类型

// boolean 布尔值
let boo: boolean = true

2.字符串类型

// string 字符串
let str: string = 'hello world'

3.数字类型

// number 数字
let num: number = 0

4.null 类型 和 undefined 类型


// undefined 表示没有定义,声明一个变量,属性是undefined, 实际用处不大
let unde: undefined = undefined;

// undefined 实际开发中,比较多的用来表示一个变量,属性,参数的可选性,未定义
let num: undefined | number = 0;

// 同样,声明一个变量是null,实际用处也不大,更多是用在联合类型中
let maybe: null | string = null;

// 同时他们是所有其它类型的子类型
let str: string;
let a: undefined = undefined;
let b: null = null;

// 类型检查是通过的,因为 undefined, null 是其它类型的子类型
str = a;
str = b;

// 很明显是报错了,实际开发中不会单独定义一个变量是undefined ,null
// 更多是一个可选,联合类型,那为typescript 什么要这样设计呢?确实有点奇怪 
str.toString(); // Uncaught TypeError: Cannot read property 'toString'

// 打开 --strictNullChecks 检查, null and undefined 只可以赋值给 unknownn, any
// 这样可以很好的规避上面的问题
let c: unknown = null;

5.never 类型

// 代表是永远不可能发生,不可能到达
// 它是任何类型的子类型
// 因为它是不可发生,不可到达的,任何其它类型都不可以作为它的子类型,any 也不可以,只能赋值给自己
// 这样做是就算赋值给never了,也没有实际意义了

// 返回是一个never 报错啦,不可能发生
function fail() {
  return error("Something failed");
}

// 很明显是一个死循环,永远不可能到达
function infiniteLoop(): never {
  while (true) {}
}

let nev1: never;
let nev2: never;
let anyType: any;
let str: string;
let num:number

// 因为它是不可以作为任何类型的子类型,只能是赋值给自己
str = nev1
num = nev1

// 任何类型都不可以作为 never 子类型的,因为没有实际意义
nev1 = anyType // error

// never 可以赋值给自己,那有什么实际用处呢?
nev1 = nev2

6.any 类型

// any 一个没有约束的推导,任何推导都成立
const word: any = 'any type will go'

// 声明 word的类型是 any, typescript 在进行类型检查时,就会认为类型推导都是成立
// 换一个讲法就是不做类型检查了,随便你怎么写都可以
// 但实际运行时,会报错 Uncaught TypeError: word.action is not a function
word.action() 

7.空类型void

// void 为一个空值,用来表示一个函数没有返回值
// 如函数没有return,实际是返回一个undefined, 
// 就是说undefined 是 void的子类型
function test():void {
  // 没有return 任何值
}

// 声明一个变量是一个void 空值,实际用处不大
let unusable: void = undefined;
// 只有关闭 `--strictNullChecks`,null 才可以赋值给void
unusable = null;

8.unknown 类型

// unknown 暂时不知道,但给我更多的信息,就可以推断出来
// 像是一个侦探,逐步收窄范围,明确类型
let maybe: unknown;

// 收窄范围是一个number 数字类型
if (typeof maybe === "number") {
  let num: number = maybe;
  // 此时maybe 是数字类型,不可以赋值给string 类型,明显不兼容
  let str:string = maybe; // error
} 

9.数组类型

// 有2种表示方式

// 方式一,推荐这种方式,更加简洁
let list: number[] = [1,2,3]

// 方式二
let arr: Array<string> = ['a','b','c']

10.元组类型

// tuple 元组
let pairs: [ string, number ] = []

11.枚举类型

// enum 枚举
enum Status{
  Draft = 1 // 如何指定,默认是从0 开始的
  Audit = 3
}
let status:Status = Status.Draft

12. Object

// Object 对象
// anything that is not number, string, boolean, bigint, symbol, null, or undefined
// 换一个说法就是js 里面基本数据类型
// 建议开发时,尽量不要使用,因为提供不了任何有用的类型信息

const obj: object = null;
// 类型推导成立的,实际运行时报错
obj.toString();

enum 枚举

javascript 没有提供枚举这个数据类型,它是typescript 的提供数据类型之一。从枚举成员的值,可以分为3类:

  • 数字枚举:每一个枚举成员的值是一个 number 数字
  • 字符串枚举:每一个枚举成员的值是一个 string 字符串
  • 混合枚举:每一个枚举成员的值,可能是一个number 数字,或者是一个 string 字符串,只是技术上支持,不建议使用
// 数字枚举
// 只初始化第一个,后面的会默认加 1
// 那么 Error 值是2,Warning 值是3,依此类推...
enum Status {
  Success = 1,
  Error, // 2
  Warning, // 3
  Info, // 4
}

// 字符串枚举
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// 字符串枚举
// 技术上支持,违反单一职责,不建议使用
enum MixDirection {
  Up = 1,
  Down = "DOWN",
}

断言

为什么要使用断言?有时候根据目前掌握的类型信息,typescript进行推导时,并没有开发者都了解更多,推导出来的不一定准确。

const el = document.getElementById("id");
// 类型报错:属性 autocomplete 不一定在 'HTMLElement' 上
// 很明显,开发者是知道el 是什么类型,比如是一个textarea,想设置使用浏览器的记忆功能自动填充文本
el.autocomplete = 'on';

// 使用断言, 告诉typescript el 是一个 HTMLTextAreaElement 类型
const el = document.getElementById("id") as HTMLTextAreaElement;
el.autocomplete = "on";

// 断言有2种表示方式,一种是关键字 as,一种是尖括号 

// as关键字方式
// 注意在jsx 中,只允许使用 as 这种方式
let someValue: unknown = "this is a string";
let strLength: number = (someValue as string).length;

// 尖括号方式
let otherValue: unknown = "this is a string";
let strLen: number = (<string>otherValue).length;

Inteface 接口

接口:是typesctipt一个核心概念,进行类型检查侧重于值的形状,兼容是基于结构子类型的。理论上interface 可以表示任何类型,比如:object对象,fuction函数, class类

如何用接口描述一个对象的类型?

从类型角度去描述,一个object对象的属性?有以下的几个方面

  1. 有哪几个属性?指数量方面的
  2. 具体一个属性:可选,还是必须要提供的
  3. 属性赋值操作:是可读还是可写
interface ObjectShape {
  // 必须要提供的属性:一个冒号 + 类型声明
  name: string;
  // 一个问号 '?':表示是一个可选属性,说明这个属性不一定存在
  sex?: number;
  // readonly 修饰符:表示只读属性,是不可以修改
  readonly height: number;
  // 可选的且只读的属性:通过readonly + '?', 2个修饰符进行组合
  readonly hobby?: string;
  // 索引签名:index type
  // 有很多属性,不能一一列举,数量方面的一个泛指
  [prop: string]: unknown;
}

如何用接口描述一个函数的类型?

// funciton shape
interface FuncShape {
  (str: string, num: number): void;
}

// 第一个参数 str与s 名称不一样,第二个参数 num 与 n 名称不一样
// typescript 在进行类型检查时,侧重的是类型,形状结构
// 只要形状一致,具名参数名,可以不一样
const func: FuncShape = function (s: string, n: number) {};

如何用接口描述一个class的类型?

一个class类,要进行类型描述主要有3个部分:

  1. 构造函数:参数的类型
  2. 属性:可见性,访问的范围控制
  3. 方法:因为方法本质就是一个函数,其它就是对函数类型的一个声明
// 在描述一个class的类型时,在心中要明确记住一点:class 的类型分2侧:静态侧和实例侧的
// 静态侧是class 本身的,实例侧是new 操作生成实例的, 要分开处理
// 用interface 有2种方法描述一个class 的类型

// 构造函数是静态方法,要单独声明
interface ConstructorShape {
  new (str: string): InstanceShape;
}

// 实例的类型
interface InstanceShape {
  print(str: string): void;
}

// 方法一:以把构造函数以参数的形式传入到另外的一个函数中
function createInstance(ctor: ConstructorShape, str: string): InstanceShape {
  return new ctor(str);
}

// 方法二:用使用类表达式,推荐你应该使用第二种,更加的直观简单
const InstacneClass: ConstructorShape = class InstacneClass
  implements InstanceShape {
  constructor(str: string) {}
  print(str: string) {
    // do something
  }
};

const instance: InstanceShape = new InstacneClass("hello world");

上面的代码只对构造函数:参数的类型进行说明,第2,3点的是实例侧的,篇幅有限,不作详细说明。可以看后面的class 类型这一个章节子类,派生类,更贴近实际开发

interface 相互之间的继承


interface Shape {
  color: string;
}

interface PenStroke {
  penWidth: number;
}

// 就这样Square 通过 extends 关键字,支持多重继承
// 从 Shape 中继承 color:sting 的类型声明
// 从 PenStroke 中继承 penWidth: number 的类型声明
interface Square extends Shape, PenStroke {
  sideLength: number;
}

interface 继承 Class

接口继承一个类的class 成员类型声明, 注意的是class 中 private 声明的成员,只能通过继承声明类得到,就也是在为它的子类。

// unique 是这个类的私有属性,只能通过成为子类获得
class Animal {
  private unique: string;
}


interface Dog extends Animal {
  run(): void;
}

// Tendy 不是 Animal 的子类,因此它是没有 unique 这个私有属性的
// 但是Dog 继承了Animal 的成员声明,必须要提供 unique
// 综上 Tendy 没有实现 Dog 接口,类型报错
class Teddy implements Dog {
  run() {
    // do something
  }
}

// Husky 是 Animal 子类,获得 unique 属性
class Husky extends Animal implements Dog {
  run() {
    // do something
  }
}

class 类的类型声明?

一个class类型检查分为2部分:一部分是:class 本身,另一部分是:子类,派生类,new 生成的实例

  1. class 本身一个静态属性/方法:使用关键字 static 声明
  2. 子类,派生类,new 生成的实例:通过puublic, private, and protected 修饰符实现
class Production {
  // 静态部分:

  constructor(readonly type: string) {} // 构造函数静态方法,原生自带
  // 静态属性
  static uuid = "__hash__";
  // 静态方法
  static action() {
    // do something
  }

  // 子类,派生类,new 生成的实例

  // 只允许在Production类内部访问
  private secret: string; // #name: string; 新的版本规范这样定义私有的
  // 放宽一点,允许在Production类内部,子类中访问
  protected serial: number;
  // 实例中访问,不作限制了
  public price: number;

  // 支行修饰符组合
  public readonly level: number = 0;

  public qualified(str: string = "") {
    console.log(`${Production.uuid === this.secret + str}`);
  }

  // 计算属性 accessors
  get fullName() {
    return "hello kitty";
  }

  set fullName(str) {
    // do something
  }
}

抽象类

要对一系列类进行抽象,同定义各个类去实现,比如:对系列手机进行抽象,手机有3个核心的功能:1.打电话 2.发信息


abstract class Phone {
  // 定义抽象的方法,要在子类中分别实现
  abstract call(): string;
  abstract message(): string;
}

class Nokia implements Phone {
  call() {
    return "Nokia is calling the number";
  }
  message() {
    return "Nokia";
  }
}

class Motorola implements Phone {
  call() {
    return "Motorola is calling the number";
  }
  message() {
    return "Motorola";
  }
}

使用一个类作为接口

// 请参考前面 interface 继承 Class
class Point {
  x: number;
  y: number;
}

interface Point3d extends Point {
  z: number;
}

let point3d: Point3d = { x: 1, y: 2, z: 3 };

Function 函数的类型

一个函数类型声明可以分为2个部分:函数的参数类型,还有return 的返回值类型

// 参数 str 类型是字符串且必须提供的
// 参数 num 是一个数字,可选参数
// 参数 sex 是一个数字,可选参数,不提供时使用默认值0
// 参数 restParams 是一个剩余参数,允许任何类型
// 函数的返回值是一个 字符串
function print(
  str: string,
  num?: number,
  sex = 0,
  ...restParams: unknown[]
): string {
  // do something
  return "hello world";
}

this 执行上下文

在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值:

  • 如果该函数是一个构造函数,this指针指向一个新的对象
  • 在严格模式下的函数调用下,this指向undefined
  • 如果该函数是一个对象的方法,则它的this指针指向这个对象
  • 等等

箭头函数的上下文:

  • 箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。
  • 箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this

interface Card {
  suit: string;
  card: number;
}

interface Deck {
  suits: string[];
  cards: number[];
  createCardPicker(this: Deck): () => Card;
}

const deck: Deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  // 尽管实际运行时是指向 deck 这个对象,
  // 但是 typescript 进行静态类型检查,要在编译成javascipt 前就要明确 this 的指向
  // 则必须在函数参数中声明 this 的指向的类型
  createCardPicker: function (this: Deck) {
    return () => {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);

      return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
    };
  },
};

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();

函数重载

// overloads 函数的重载

function pick(input: string): string;
function pick(input: number): number;
// 只允许sting , number
// 最后一个,只是一个兼容声明,
function pick(input: unknown): unknown {
  // 输入的参数是字符串
  if (typeof input === "string") {
    return "我是一个字符串";
  }

  // 输入的参数是数字
  if (typeof input === "number") {
    return input + 10;
  }
  // 布尔值
  return true;
}

let str: string = pick("hey");
let num: number = pick(2);

// 不在重载的类型范围中,类型报错
let other: boolean = pick(true); // error

泛型

一个'泛'字,足以说明泛指一个类型范围。实际上泛型就是一开始不指定具体的类型,之后通过类型参数明确具体的类型,然后通过类型关系处理明确类型范围。

泛型的2个关键点:

  • 类型参数:类型以参数形式传递给对应泛型声明,有的泛型会提供默认参数类型,使用时可以不提供类型参数
  • 类型关系:类型传给泛型,可以直接使用,也可以进一步处理,得到不一样的类型范围

泛型之所以被认为复杂,主要是类型关系的处理,类型参数的不一样,使类型发生变化。不过,明确上面的提供的2点,其实也很好把握。

// 在接口 interface 中使用泛型

/**
 * 首先明确一点,T 是一个类型参数
 * T extends string 表示:类型参数 T 满足于 string, 从而对输入的参数进行约束
 */
interface Animal<T extends string> {
  // 每一个键的类型是string,
  // 对应值是:每一个元素为T类型的数组
  [k: string]: T[];
}

// 字符串 abc 是 string 类型中的一个子类型,符合对类型参数T约束
const dog: Animal<"abc"> = {
  list: ["abc"],
};

// 在 class 类中使用泛型

/**
 * T 是一个类型参数
 */
class GenericObject<T> {
  // zeroValue 属性的类型是 T
  zeroValue: T;
  // 参数 x 的类型是 T
  // 参数 y 的类型是 T
  // 返回值类型也是 T
  add: (x: T, y: T) => T;
}

// 给类型参数赋值为 number 类型
const numObj: GenericObject<number> = {
  zeroValue: 0,
  add(x: number, y: number): number {
    return x + y;
  },
};

类型别名

类型别名:本质上是为类型声明指定一个名称,是类型声明的一个引用。在官方文档中,叫别名给人的感觉,就是一个类型的已经存在,只是另起一个新名称。的确,type alias 类型别名,在实际开发,对已有的类型的扩展,重新组合,声明联合或元组类型,更推荐使用别名。

// 给 number 类型指定一个声明
type numRef = number;
const num: numRef = 10;

// 给泛型类型声明指定一个名称
type GenericType<T = number> = T;

type Tree<T = string> = {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
};

// 给对已有类型的重新组合,指定一个名称
type CompositeType = GenericType & Tree;

接口 vs 类型别名?

接口和类型别名在语法上,比较相似。很的必要放在一起加以比较以加深理解。类型别名是本质上是类型引用,他们主要有2个方面的区别?

  1. 使用一个新名称,对已有类型的扩展,他们是一样的
  2. 直接在原类型上扩展,名称不变

区别一:

interface Animal {
  name: string
}

// 定义了一个新接口 Bear
interface Bear extends Animal {
  honey: boolean
}

const bear = getBear() 
bear.name
bear.honey
type Animal = {
  name: string
}

// 定义了一个新类型 Bear
type Bear = Animal & { 
  honey: Boolean 
}

const bear = getBear();
bear.name;
bear.honey;

区别二:

interface Window {
  title: string
}
// 接口进行合并,就是merge
interface Window {
  ts: import("typescript")
}

const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
type Window = {
  title: string
}

// 不能进行合并,2个名称都叫 Window 发生命名冲突
type Window = {
  ts: import("typescript")
}

// Error: Duplicate identifier 'Window'.

typscript 文档中指出:因为interface 更加接近javascript 对象对扩展的开放,如果可能,我们推荐使用interfade. 从另一方面,如果你不能只使用一个接口表达出类型的形状,你需要使用别名去处理联合类型,元祖类型,这也是我们经常使用的方式。

接口interface, 类型别名的使用建议

  • 优先使用interface: 如果是单一接口,并可能在原基础上扩展,且符合单一职责
  • 优先使用类型别名:对已有类型扩展,侧重的是类型关系重构实现扩展

结束语

有兴趣的可以看一下,typescript 基础知识,试写一下, 侧重的是对基础的练习。