AnnGreen1 / article

0 stars 0 forks source link

一篇让你完全够用的TS指南 #11

Open AnnGreen1 opened 4 months ago

AnnGreen1 commented 4 months ago

大家好,我叫小杜杜,是一个喜欢动手敲代码的小菜鸟,我认为代码应该亲自敲一遍,才能更好的熟记于心,作为一个程序员,我们与自己的小伙伴共同开发、维护于一个项目,需要保持代码的整洁、清晰,使组内的任意一位小伙伴都能快速理解每一代码模块,因此制定了各种各样的规则,去约束我们写代码,只有这样才能我们的代码更加具有可读性可维护性健壮性

我们用什么去管理自己的项目,在前几年中,我们使用eslint,用prop-types插件去定义参数的类型,当我接触的时候,我本人是非常非常抵触的,为什么呢?因为我个人觉得非常的麻烦,我现在还记得,当时在老项目装接入eslint满篇爆红,去查为何爆红的场景,想想真是惨绝人寰~

之后接触到TS,一开始本人也是非常抵触,但抱着试一试的态度,去学习,使用它,你会发现越用越好用,甚至觉得不用TS写代码都不爽了,当然有了TS你就可以远离eslintprop-types了。

如果你准备接触 TS,或者刚接触,对 TS 不太理解相信这篇文章一定能更好的帮助你,希望大家多多支持~

先来看看知识图,如果你对以下概念有盲区,那么这篇文章应该能很好的帮助到你~

image

TS 是什么 ?

TS:是TypeScript的简称,是一种由微软开发的自由和开源的编程语言。

TS 和 JS 的关系

对比于 JS,TS 是 JS 的超集,简单的说就是在 JavaScript 的基础上加入了类型系统,让每个参数都有明确的意义,从而带来了更加智能的提示。

相对于JS而言,TS属于强类型语言,所以对于项目而言,会使代码更加规范,从而解决了大型项目代码的复杂性,其次,浏览器是不识别TS的,所以在编译的时候,TS文件会先编译为JS文件。

安装 TS

执行命令:

$ npm install -g typescript
//或
$ yarn global add typescript

查看版本

$ tsc -v

编译

$ tsc test.ts
# test.ts => test.js

在线编译

我们为了方便起见,可以使用线上的编辑器:TypeScript Playground,像这样

image 并且你还可以看看生成对应的ts转化ES5ES6之后的代码,也有相关的例子供你查看

TS 的基本数据类型

这里将 TS 的数据类型简单的进行下归类:

注:案例中有可能用到typeinterface,在下面会详细讲解,有比较模糊的可以先看看

基本类型

//字符串
let str: string = "Domesy";

// 数字
let num: number = 7;

//布尔
let bool: boolean = true;

//symbol
let sym: symbol = Symbol();

//bigint
let big: bigint = 10n;

//null
let nu: null = null;

//undefined
let un: undefined = undefined;

需要注意:

因为 TS 最大的特点就是类型的问题,不只是nullundefined,其他类型也是这样的(但是肯定可以赋值给any)。@AnnGreen1

let num: number = 9;
let str: string = "xxx";
num = str; // error TS2322: Type 'string' is not assignable to type 'number'.
console.log(num);

引用类型

Array

两种方式:

let arr1: number[] = [1, 2, 3];

let arr2: Array<number> = [1, 2, 3];

let arr2: Array<number> = [1, 2, "3"]; // error

//要想是数字类型或字符串类型,需要使用 |
let arr3: Array<number | string> = [1, 2, "3"]; //ok

Tuple(元组)

Tuple 可以说是 Array 的一种特殊情况,针对上面的 arr3,我们看他的类型可以是string也可以是number,但对每个元素没有作出具体的限制。

那么 Tuple 的作用就是限制元素的类型并且限制个数的数组,同时 Tuple这个概念值存在于TS,在JS上是不存在的

每一个元素类型要符合 Tuple 的限制(先后顺序上)@AnnGreen1

let t: [number, string] = ["1", "1"]; // error TS2322: Type 'string' is not assignable to type 'number'.

这里存在一个问题:在TS中,是允许对 Tuple 扩增的(也就是允许使用 push方法),但在访问上不允许

let t: [number, string] = [1, "2"]; // ok
let t1: [number, string] = [1, 3]; // error
let t2: [number, string] = [1]; // error
let t3: [number, string] = [1, "1", true]; // error

let t5: [number, string] = [1, "2"]; // ok
t.push(2);
console.log(t); // [1, '2', 2]

let a = t[0]; // ok
let b = t[1]; // ok
let c = t[2]; // error

object

let obj1: object = { a: 1, b: 2 };
obj1.a = 3; // error

let obj2: { a: number; b: number } = { a: 1, b: 2 };
obj2.a = 3; // ok
let obj: Object;
obj = 1; // ok
obj = "a"; // ok
obj = true; // ok
obj = {}; // ok
obj = Symbol(); //ok
obj = 10n; //ok
obj = null; // error
obj = undefined; // error

function

定义函数

function setName1(name: string) {
  //ok
  console.log("hello", name);
}
setName1("Domesy"); // "hello",  "Domesy"

function setName2(name: string): string {
  //error
  console.log("hello", name);
}
setName2("Domesy");

function setName3(name: string): string {
  //error
  console.log("hello", name);
  return 1;
}
setName3("Domesy");

function setName4(name: string): string {
  //ok
  console.log("hello", name);
  return name;
}
setName4("Domesy"); // "hello",  "Domesy"

//箭头函数与上述同理
const setName5 = (name: string) => console.log("hello", name);
setName5("Domesy"); // "hello",  "Domesy"

参数类型

// 可选参数
const setInfo1 = (name: string, age?: number) => console.log(name, age);
setInfo1("Domesy"); //"Domesy",  undefined
setInfo1("Domesy", 7); //"Domesy",  7

// 默认参数
const setInfo2 = (name: string, age: number = 11) => console.log(name, age);
setInfo2("Domesy"); //"Domesy",  11
setInfo2("Domesy", 7); //"Domesy",  7

// 剩余参数
const allCount = (...numbers: number[]) =>
  console.log(`数字总和为:${numbers.reduce((val, item) => (val += item), 0)}`);
allCount(1, 2, 3); //"数字总和为:6"

函数重载

函数重载:是使用相同名称和不同参数数量或类型创建多个方法的一种能力。 在 TypeScript 中,表现为给同一个函数提供多个函数类型定义。 简单的说:可以在同一个函数下定义多种类型值,最后汇总到一块

let obj: any = {};
function setInfo(val: string): void;
function setInfo(val: number): void;
function setInfo(val: boolean): void;
function setInfo(val: string | number | boolean): void {
  if (typeof val === "string") {
    obj.name = val;
  } else {
    obj.age = val;
  }
}
setInfo("Domesy");
setInfo(7);
setInfo(true);
console.log(obj); // { name: 'Domesy', age: 7 }

特殊类型

any

在 TS 中,任何类型都可以归于 any 类型,所以any类型也就成了所有类型的顶级类型,同时,如果不指定变量的类型,则默认为 any 类型, 当然不推荐使用该类型,因为这样丧失了 TS 的作用

let d: any; //等价于 let d
d = "1";
d = 2;
d = true;
d = [1, 2, 3];
d = {};

unknow

any一样,都可以作为所有类型的顶级类型,但 unknow更加严格,那么可以说除了any 之下的第二大类型,接下来对比下any,主要严格于一下两点:

let u: unknown;
let a: any;

u = "1"; //ok
u = 2; //ok
u = true; //ok
u = [1, 2, 3]; //ok
u = {}; //ok

let value: any = u; //ok
let value1: any = a; //ok
let value2: unknown = u; //ok
let value3: unknown = a; //ok
let value4: string = u; //error
let value5: string = a; //ok
let value6: number = u; //error
let value7: number = a; //ok
let value8: boolean = u; //error
let value9: boolean = a; //ok

u.set(); // error
a.set(); //ok
u(); // error
a(); //ok
new u(); // error
new a(); //ok

void

当一个函数,没有返回值时,TS 会默认他的返回值为 void 类型

const setInfo = (): void => {}; // 等价于 const setInfo = () => {}

const setInfo1 = (): void => {
  return "1";
}; // error
const setInfo2 = (): void => {
  return 2;
}; // error
const setInfo3 = (): void => {
  return true;
}; // error
const setInfo4 = (): void => {
  return;
}; // ok
const setInfo5 = (): void => {
  return undefined;
}; //ok

never

表示一个函数永远不存在返回值,TS 会认为类型为 never,那么与 void 相比, never应该是 void子集, 因为 void实际上的返回值为 undefined,而 neverundefined也不行

符合never的情况有:当抛出异常的情况和无限死循环

let error = (): never => {
  // 等价约 let error = () => {}
  throw new Error("error");
};

let error1 = (): never => {
  while (true) {}
};

Enum(枚举)

可以定义一些带名字的常量,这样可以清晰表达意图创建一组有区别的用例

注意:

同时我们可以看看翻译为 ES5 是何样子

数字枚举

image

@AnnGreen1

enum numberType {
  A,
  B,
  C = 7,
  D,
}

let num = numberType.A;
let num1 = numberType[0];
let num2 = numberType[7];
let num3 = numberType[8];

console.log(num); // 0
console.log(num1); // A
console.log(num2); // C
console.log(num3); // D

字符串枚举

字符串枚举要注意的是必须要有默认值,不支持反向映射

image

常量枚举

除了数字类型字符串类型之外,还有一种特殊的类型,那就是常量枚举,也就是通过const去定义enum,但这种类型不会编译成任何 JS,只会编译对应的值

image

异构枚举

包含了 数字类型字符串类型 的混合,反向映射一样的道理

image

类型推论

我们在学完这些基础类型,我们是不是每个类型都要去写字段是什么类型呢?其实不是,在TS中如果不设置类型,并且不进行赋值时,将会推论为any类型,如果进行赋值就会默认为类型

let a; // 推断为any
let str = "小杜杜"; // 推断为string
let num = 13; // 推断为number
let flag = false; // 推断为boolean

str = true; // error Type 'boolean' is not assignable to type 'string'.(2322)
num = "Domesy"; // error
flag = 7; // error

字面量类型

字面量类型:在TS中,我们可以指定参数的类型是什么,目前支持字符串数字布尔三种类型。比如说我定义了 str 的类型是 '小杜杜' 那么 str 的值只能是小杜杜

let str: "小杜杜";
let num: 1 | 2 | 3 = 1;
let flag: true;

str = "小杜杜"; //ok
str = "Donmesy"; // error

num = 2; //ok
num = 7; // error

flag = true; // ok
flag = false; // error

交叉类型(&)

交叉类型:将多个类型合并为一个类型,使用&符号连接,如:

type AProps = { a: string };
type BProps = { b: number };

type allProps = AProps & BProps;

const Info: allProps = {
  a: "小杜杜",
  b: 7,
};

同名基础属性合并

我们可以看到交叉类型是结合两个属性的属性值,那么我们现在有个问题,要是两个属性都有相同的属性值,那么此时总的类型会怎么样,先看看下面的案列:

type AProps = { a: string; c: number };
type BProps = { b: number; c: string };

type allProps = AProps & BProps;

const Info: allProps = {
  a: "小杜杜",
  b: 7,
  c: 1, // error (property) c: never
  c: "Domesy", // error (property) c: never
};

如果是相同的类型,合并后的类型也是此类型,那如果是不同的类型会如何:

我们在ApropsBProps中同时加入c属性,并且c属性的类型不同,一个是number类型,另一个是string类型

现在结合为 allProps 后呢? 是不是c属性numberstring 类型都可以,还是其中的一种?

然而在实际中, c 传入数字类型字符串类型都不行,我么看到报错,现实的是 c 的类型是 never

这是因为对应 c属性而言是 string & number,然而这种属性明显是不存在的,所以c的属性是never

同名非基础属性合并

interface A {
  a: number;
}
interface B {
  b: string;
}

interface C {
  x: A;
}
interface D {
  x: B;
}
type allProps = C & D;

const Info: allProps = {
  x: {
    a: 7,
    b: "小杜杜",
  },
};

console.log(Info); // { x: { "a": 7, "b": "小杜杜" }}

我们来看看案例,对于混入多个类型时,若存在相同的成员,且成员类型为非基本数据类型,那么是可以成功合。

如果 接口 A 中的 也是 b,类型为 number,就会跟同名基础属性合并一样

Class(类)

ES6中推出了一个叫 class(类) 的玩意,具体定义就不说了,相信用过React的小伙伴一定不陌生.

基本方法

在基本方法中有:静态属性静态方法成员属性成员方法构造器get set方法,接下来逐个看看:

需要注意的是: 在成员属性中,如果不给默认值,并且不使用是会报错的,如果不想报错就给如 !,如:name4!:string

class Info {
  //静态属性
  static name1: string = "Domesy";

  //成员属性,实际上是通过public上进行修饰,只是省略了
  nmae2: string = "Hello"; //ok
  name3: string; //error
  name4!: string; //ok 不设置默认值的时候必须加入 !

  //构造方法
  constructor(_name: string) {
    this.name4 = _name;
  }

  //静态方法
  static getName = () => {
    return "我是静态方法";
  };

  //成员方法
  getName4 = () => {
    return `我是成员方法:${this.name4}`;
  };

  //get 方法
  get name5() {
    return this.name4;
  }

  //set 方法
  set name5(name5) {
    this.name4 = name5;
  }
}

const setName = new Info("你好");
console.log(Info.name1); //  "Domesy"
console.log(Info.getName()); // "我是静态方法"
console.log(setName.getName4()); // "我是成员方法:你好"

让我们看看上述代码翻译成 ES5 是什么样:

"use strict";
var Info = /** @class */ (function () {
  //构造方法
  function Info(_name) {
    var _this = this;
    //成员属性
    this.nmae2 = "Hello"; //ok
    //成员方法
    this.getName4 = function () {
      return "\u6211\u662F\u6210\u5458\u65B9\u6CD5:".concat(_this.name4);
    };
    this.name4 = _name;
  }
  Object.defineProperty(Info.prototype, "name5", {
    //get 方法
    get: function () {
      return this.name4;
    },
    //set 方法
    set: function (name5) {
      this.name4 = name5;
    },
    enumerable: false,
    configurable: true,
  });
  //静态属性
  Info.name1 = "Domesy";
  //静态方法
  Info.getName = function () {
    return "我是静态方法";
  };
  return Info;
})();
var setName = new Info("你好");
console.log(Info.name1); //  "Domesy"
console.log(Info.getName()); // "我是静态方法"
console.log(setName.getName4()); // "我是成员方法:你好"

私有字段(#)

在 TS 3.8 版本便开始支持ECMACMAScript的私有字段。

需要注意的是私有字段与常规字段不同,主要的区别是:

class Info {
  #name: string; //私有字段
  getName: string;

  constructor(name: string) {
    this.#name = name;
    this.getName = name;
  }

  setName() {
    return `我的名字是${this.#name}`;
  }
}

let myName = new Info("Domesy");

console.log(myName.setName()); // "我的名字是Domesy"
console.log(myName.getName); // ok "Domesy"
console.log(myName.#name); // error
// Property '#name' is not accessible outside class 'Info'
// because it has a private identifier.(18013)

只读属性(readonly)

只读属性:用 readonly修饰,只能在构造函数中初始化,并且在 TS 中,只允许将interfacetypeclass上的属性标识为readonly

class Info {
  public readonly name: string; // 只读属性
  name1: string;

  constructor(name: string) {
    this.name = name;
    this.name1 = name;
  }

  setName(name: string) {
    this.name = name; // error
    this.name1 = name; // ok
  }
}

继承(extends)

继承:是个比较重要的点,指的是子可以继承父的思想,也就是说 子类 通过继承父类后,就拥有了父类的属性和方法,这点与HOC有点类似

这里又个super字段,给不知道的小伙伴说说,其作用是调用父类上的属性和方法

// 父类
class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  getName() {
    console.log(`我的姓名是:${this.name}`);
    return this.name;
  }

  setName(name: string) {
    console.log(`设置姓名为:${name}`);
    this.name = name;
  }
}

// 子类
class Child extends Person {
  tel: number;
  constructor(name: string, age: number, tel: number) {
    super(name, age);
    this.tel = tel;
  }

  getTel() {
    console.log(`电话号码是${this.tel}`);
    return this.tel;
  }
}

let res = new Child("Domesy", 7, 123456);
console.log(res); // Child {."name": "Domesy", "age": 7, "no": 1 }
console.log(res.age); // 7
res.setName("小杜杜"); // "设置姓名为:小杜杜"
res.getName(); //   "我的姓名是:小杜杜"
res.getTel(); //  "电话号码是123456"

修饰符

主要有三种修饰符:

class Person {
  public name: string;
  protected age: number;
  private tel: number;

  constructor(name: string, age: number, tel: number) {
    this.name = name;
    this.age = age;
    this.tel = tel;
  }
}

class Child extends Person {
  constructor(name: string, age: number, tel: number) {
    super(name, age, tel);
  }

  getName() {
    console.log(`我的名字叫${this.name},年龄是${this.age}`); // ok name 和 age可以
    console.log(`电话是${this.tel}`); // error 报错 原因是 tel 拿不出来
  }
}

const res = new Child("Domesy", 7, 123456);
console.log(res.name); // ok Domesy
console.log(res.age); // error
console.log(res.tel); // error

abstract

abstract: 用 abstract 关键字声明的类叫做抽象类,声明的方法叫做抽象方法

注:抽象类是不能直接实例化,只能实例化实现了所有抽象方法的子类

abstract class Person {
  constructor(public name: string) {}

  // 抽象方法
  abstract setAge(age: number): void;
}

class Child extends Person {
  constructor(name: string) {
    super(name);
  }

  setAge(age: number): void {
    console.log(`我的名字是${this.name},年龄是${age}`);
  }
}

let res = new Person("小杜杜"); //error
let res1 = new Child("小杜杜");

res1.setAge(7); // "我的名字是小杜杜,年龄是7"

重写和重载

// 重写
class Person {
  setName(name: string) {
    return `我的名字叫${name}`;
  }
}

class Child extends Person {
  setName(name: string) {
    return `你的名字叫${name}`;
  }
}

const yourName = new Child();
console.log(yourName.setName("小杜杜")); // "你的名字叫小杜杜"

// 重载
class Person1 {
  setNameAge(name: string): void;
  setNameAge(name: number): void;
  setNameAge(name: string | number) {
    if (typeof name === "string") {
      console.log(`我的名字是${name}`);
    } else {
      console.log(`我的年龄是${name}`);
    }
  }
}

const res = new Person1();
res.setNameAge("小杜杜"); // "我的名字是小杜杜"
res.setNameAge(7); // "我的年龄是7"

TS 断言和类型守卫

TS 断言

分为三种:类型断言非空断言确定赋值断言

当断言失效后,可能使用到:双重断言

类型断言

在特定的环境中,我们会比 TS 知道这个值具体是什么类型,不需要 TS 去判断,简单的理解就是,类型断言会告诉编译器,你不用给我进行检查,相信我,他就是这个类型

共有两种方式:

//尖括号
let num: any = "小杜杜";
let res1: number = (<string>num).length; // React中会 error

// as 语法
let str: any = "Domesy";
let res: number = (str as string).length;

但需要注意的是:尖括号语法在React中会报错,原因是与JSX语法会产生冲突,所以只能使用as 语法

非空断言

在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符 ! 可以用于断言操作对象是非 null 和非 undefined 类型。

我们对比下ES5的代码

image

我们可以看出来 !可以帮助我们过滤 nullundefined类型,也就是说,编译器会默认我们只会传来string类型的数据,所以可以赋值为str1

但变成ES5!会被移除,所以当传入 null 的时候,还是会打出 null

确定赋值断言

TS 2.7 版本中引入了确定赋值断言,即允许在实例属性和变量声明后面放置一个 ! 号,以告诉TS该属性会被明确赋值。

let num: number;
let num1!: number;

const setNumber = () => (num = 7);
const setNumber1 = () => (num1 = 7);

setNumber();
setNumber1();

console.log(num); // error
console.log(num1); // ok

双重断言

断言失效后,可能会用到,但一般情况下不会使用

失效的情况:基础类型不能断言为接口

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

const name = "小杜杜" as Info; // error, 原因是不能把 string 类型断言为 一个接口
const name1 = "小杜杜" as any as Info; //ok

类型守卫

类型守卫:是可执行运行时检查的一种表达式,用于确保该类型在一定的范围内

我个人的感觉是,类型守卫就是你可以设置多种类型,但我默认你是什么类型的意思

目前,常有的类型守卫共有 4 种:in 关键字typeof 关键字instanceof类型谓词(is)

in 关键字

用于判断这个属性是那个里面的

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

interface Info1 {
  name: string;
  flage: true;
}

const setInfo = (data: Info | Info1) => {
  if ("age" in data) {
    console.log(`我的名字是:${data.name},年龄是:${data.age}`);
  }

  if ("flage" in data) {
    console.log(`我的名字是:${data.name},性别是:${data.flage}`);
  }
};

setInfo({ name: "小杜杜", age: 7 }); // "我的名字是:小杜杜,年龄是:7"
setInfo({ name: "小杜杜", flage: true }); // "我的名字是:小杜杜,性别是:true"

typeof 关键字

用于判断基本类型,如 string | number 等

const setInfo = (data: number | string | undefined) => {
  if (typeof data === "string") {
    console.log(`我的名字是:${data}`);
  }

  if (typeof data === "number") {
    console.log(`我的年龄是:${data}`);
  }

  if (typeof data === "undefined") {
    console.log(data);
  }
};

setInfo("小杜杜"); // "我的名字是:小杜杜"
setInfo(7); // "我的年龄是:7"
setInfo(undefined); // undefined"

instanceof 关键字

用于判断一个实例是不是构造函数,或使用类的时候

class Name {
  name: string = "小杜杜";
}

class Age extends Name {
  age: number = 7;
}

const setInfo = (data: Name) => {
  if (data instanceof Age) {
    console.log(`我的年龄是${data.age}`);
  } else {
    console.log(`我的名字是${data.name}`);
  }
};

setInfo(new Name()); // "我的名字是小杜杜"
setInfo(new Age()); // "我的年龄是7"

类型谓词(is)

function isNumber(x: any): x is number {
  //默认传入的是number类型
  return typeof x === "number";
}

console.log(isNumber(7)); // true
console.log(isNumber("7")); //false
console.log(isNumber(true)); //false

两者的区别

通过上面的介绍,我们可以发现断言类型守卫的概念非常相似,都是确定参数的类型,但断言更加霸道,它是直接告诉编辑器,这个参数就是这个类型,而类型守卫更像确定这个参数具体是什么类型。(个人理解,有不对的地方欢迎指出~)

类型别名、接口

类型别名(type)

类型别名:也就是type,用来给一个类型起个新名字

type InfoProps = string | number;

const setInfo = (data: InfoProps) => {};

接口(interface)

接口:在面向对象语言中表示行为抽象,也可以用来描述对象的形状。

使用interface关键字来定义接口

对象的形状

接口可以用来描述对象,主要可以包括以下数据:可读属性只读属性任意属性

  interface Props {
        a: string;
        b: number;
        c: boolean;
        d?: number; // 可选属性
        readonly e: string; //只读属性
        [f: string]: any //任意属性
    }
    let res: Props = {
        a: '小杜杜',
        b: 7,
        c: true,
        e: 'Domesy',
        d: 1, // 有没有d都可以
        h: 2 // 任意属性,之前为定义过h
    }

    let res.e = 'hi' // error, 原因是可读属性不允许更

继承

继承:与类一样,接口也存在继承属性,也是使用extends字段

interface nameProps {
  name: string;
}

interface Props extends nameProps {
  age: number;
}

const res: Props = {
  name: "小杜杜",
  age: 7,
};

函数类型接口

同时,可以定义函数和类,加new修饰的是,不加 new 的是函数

interface Props {
  (data: number): number;
}

const info: Props = (number: number) => number; //可定义函数

// 定义函数
class A {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

interface PropsClass {
  new (name: string): A;
}

const info1 = (fun: PropsClass, name: string) => new fun(name);

const res = info1(A, "小杜杜");
console.log(res.name); // "小杜杜"

tips: 这就是所谓的类型体操吧。。。

type 和 interface 的区别

通过上面的学习,我们发现类型别名接口非常相似,可以说在大多数情况下,typeinterface是等价的

但在一些特定的场景差距还是比较大的,接下来逐个来看看

基础数据类型

type A = string; // 基本类型

type B = string | number; // 联合类型

type C = [number, string]; // 元祖

const dom = document.createElement("div"); // dom元素
type D = typeof dom;

扩展

interface 可以扩展 typetype 也可以扩展为 interface,但两者实现扩展的方式不同。

// interface 扩展 interface
interface A {
  a: string;
}
interface B extends A {
  b: number;
}
const obj: B = { a: `小杜杜`, b: 7 };

// type 扩展 type
type C = { a: string };
type D = C & { b: number };
const obj1: D = { a: `小杜杜`, b: 7 };

// interface 扩展为 Type
type E = { a: string };
interface F extends E {
  b: number;
}
const obj2: F = { a: `小杜杜`, b: 7 };

// type 扩展为 interface
interface G {
  a: string;
}
type H = G & { b: number };
const obj3: H = { a: `小杜杜`, b: 7 };

重复定义

interface 可以多次被定义,并且会进行合并,但type不行

interface A {
  a: string;
}
interface A {
  b: number;
}
const obj: A = { a: `小杜杜`, b: 7 };

type B = { a: string };
type B = { b: number }; // error

联合类型(Union Types)

联合类型(Union Types): 表示取值可以为多种类型中的一种,未赋值时联合类型上只能访问两个类型共有的属性和方法,如:

const setInfo = (name: string | number) => {};

setInfo("小杜杜");
setInfo(7);

从上面看 setInfo接收一个name,而 name 可以接收 stringnumber类型,那么这个参数便是联合类型

可辨识联合

可辨识联合:包含三个特点,分别是可辨识联合类型类型守卫,

这种类型的本质是:结合联合类型字面量类型的一种类型保护方法。

如果一个类型是多个类型的联合类型,且多个类型含有一个公共属性,那么就可以利用这个公共属性,来创建不同的类型保护区块。

也就是上面一起结合使用,这里写个小例子:

interface A {
  type: 1;
  name: string;
}

interface B {
  type: 2;
  age: number;
}

interface C {
  type: 3;
  sex: boolean;
}

// const setInfo = (data: A | B | C) => {
//   return data.type // ok 原因是 A 、B、C 都有 type属性
//   return data.age // error,  原因是没有判断具体是哪个类型,不能确定是A,还是B,或者是C
// }

const setInfo1 = (data: A | B | C) => {
  if (data.type === 1) {
    console.log(`我的名字是${data.name}`);
  } else if (data.type === 2) {
    console.log(`我的年龄是${data.age}`);
  } else if (data.type === 3) {
    console.log(`我的性别是${data.sex}`);
  }
};

setInfo1({ type: 1, name: "小杜杜" }); // "我的名字是小杜杜"
setInfo1({ type: 2, age: 7 }); // "我的年龄是7"
setInfo1({ type: 3, sex: true }); // "我的性别是true"

定义了 ABC 三次接口,但这三个接口都包含type属性,那么type就是可辨识的属性,而其他属性只跟特性的接口相关。

然后通过可辨识属性type,才能使用其相关的属性

泛型

泛型:Generics,是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性

也就是说,泛型是允许同一个函数接受不同类型参数的一种模版,与any相比,使用泛型来创建可复用的组件要更好,因为泛型会保留参数类型(PS:泛型是整个 TS 的重点,也是难点,请多多注意~)

为什么需要泛型

我们先看看一个例子:

const calcArray = (data: any): any[] => {
  let list = [];
  for (let i = 0; i < 3; i++) {
    list.push(data);
  }
  return list;
};

console.log(calcArray("d")); // ["d", "d", "d"]

上述的例子我们发现,在calcArray中传任何类型的参数,返回的数组都是any类型

由于我们不知道传入的数据是什么,所以返回的数据也为any的数组

但我们现在想要的效果是:无论我们传什么类型,都能返回对应的类型,针对这种情况怎么办?所以此时泛型就登场了

泛型语法

我们先用泛型对上面的例子进行改造下,

const calcArray = <T>(data: T): T[] => {
  let list: T[] = [];
  for (let i = 0; i < 3; i++) {
    list.push(data);
  }
  return list;
};

const res: string[] = calcArray<string>("d"); // ok
const res1: number[] = calcArray<number>(7); // ok

type Props = {
  name: string;
  age: number;
};
const res3: Props[] = calcArray<Props>({ name: "小杜杜", age: 7 }); //ok

经过上面的案例,我们发现传入的字符串数字对象,都能返回对应的类型,从而达到我们的目的,接下来我们再看看泛型语法

function identity<T>(value: T): T {
  return value;
}

第一次看到这个<T>我们是不是很懵,实际上这个T就是传递的类型,从上述的例子来看,这个<T>就是<string>,要注意一点,这个<string>实际上是可以省略的,因为 TS 具有类型推论,可以自己推断类型

tips:大概理解一下,泛型就是不明确指定是什么具体的类型,但是规定了都是T类型(这是一个可变的,所以说是没有明确指明的),这个规定的类型在需要使用的时候进行“占位”,实际上把'string''number'字符串当作参数、甚至直接使用1 2 3...这样来区分也未尝不可

多类型传参

我们有多个未知的类型占位,我们可以定义任何的字母来表示不同的参数类型

const calcArray = <T, U>(name: T, age: U): { name: T; age: U } => {
  const res: { name: T; age: U } = { name, age };
  return res;
};

const res = calcArray<string, number>("小杜杜", 7);
console.log(res); // {"name": "小杜杜", "age": 7}

泛型接口

定义接口的时候,我们也可以使用泛型

interface A<T> {
  data: T;
}

const Info: A<string> = { data: "1" };
console.log(Info.data); // "1"

泛型类

同样泛型也可以定义类

class clacArray<T> {
  private arr: T[] = [];

  add(value: T) {
    this.arr.push(value);
  }
  getValue(): T {
    let res = this.arr[0];
    console.log(this.arr);
    return res;
  }
}

const res = new clacArray();

res.add(1);
res.add(2);
res.add(3);

res.getValue(); //[1, 2, 3]
console.log(res.getValue); // 1

泛型类型别名

type Info<T> = {
  name?: T;
  age?: T;
};

const res: Info<string> = { name: "小杜杜" };
const res1: Info<number> = { age: 7 };

泛型默认参数

所谓默认参数,是指定类型,如默认值一样,从实际值参数中也无法推断出类型时,这个默认类型就会起作用。

const calcArray = <T = string>(data: T): T[] => {
  let list: T[] = [];
  for (let i = 0; i < 3; i++) {
    list.push(data);
  }
  return list;
};

泛型常用字母

用常用的字母来表示一些变量的代表:

常用技巧

在 TS 中有许多关键字和工具类型,在使用上,需要注意泛型上的应用,有的时候结合起来可能就有一定的问题

在此特别需要注意 extendstypeofPartialRecordExcludeOmit这几个工具类型

extends

extends:检验是否拥有其属性 在这里,举个例子,我们知道字符串数组拥有length属性,但number没有这个属性。

const calcArray = <T>(data: T): number => {
  return data.length; // error
};

上述的 calcArray的作用只是获取data的数量,但此时在TS中会报错,这是因为TS 不确定传来的属性是否具备 length 这个属性,毕竟每个属性都不可能完全相同

那么这时该怎么解决呢?

我们已经确定,要拿到传过来数据的 length,也就是说传过来的属性必须具备length这个属性,如果没有,则不让他调用这个方法。

换句话说,calcArray需要具备检验属性的功能,对于上述例子就是检验是否有length的功能,这是我们就需要extends这个属性帮我们去鉴定:

interface Props {
  length: number;
}

const calcArray = <T extends Props>(data: T): number => {
  return data.length; // error
};

calcArray("12"); // ok
calcArray([1, 3]); //ok
calcArray(2); //error

可以看出calcArray(2)会报错,这是因为number类型并不具备length这个属性

typeof

typeof 关键字:我们在类型保护的时候讲解了 typeof 的作用,除此之外,这个关键字还可以实现推出类型,如下图,可以推断中 Props 包含的类型

image

keyof

keyof 关键字: 可以获取一个对象接口的所有key值,可以检查对象上的键是否存在

interface Props {
  name: string;
  age: number;
  sex: boolean;
}

type PropsKey = keyof Props; //包含 name, age, sex

const res: PropsKey = "name"; // ok
const res1: PropsKey = "tel"; // error

// 泛型中的应用
const getInfo = <T, K extends keyof T>(data: T, key: K): T[K] => {
  return data[key];
};

const info = {
  name: "小杜杜",
  age: 7,
  sex: true,
};

getInfo(info, "name"); //ok
getInfo(info, "tel"); //error

索引访问操作符

索引访问操作符:通过 [ ] 操作符可进行索引访问,可以访问其中一个属性

image

in

in:映射类型, 用来映射遍历枚举类型

image

infer

infer:可以是使用为条件语句,可以用 infer 声明一个类型变量并且对它进行使用。如

type Info<T> = T extends { a: infer U; b: infer U } ? U : never;

type Props = Info<{ a: string; b: number }>; // Props类: string | number

type Props1 = Info<number>; // Props类型: never

Partial

Partial 语法Partial<T> 作用:将所有属性变为可选的 ?

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

const info: Props = {
  name: "小杜杜",
  age: 7,
};

const info1: Partial<Props> = {
  name: "小杜杜",
};

从上述代码上来看,name 和 age 属于必填,对于 info 来说必须要设置 name 和 age 属性才行,但对于 info1 来说,只要是个对象就可以,至于是否有 name、 age 属性并不重要

Required

Required 语法Required<T> 作用:将所有属性变为必选的,与 Partial相反

interface Props {
  name: string;
  age: number;
  sex?: boolean;
}

const info: Props = {
  name: "小杜杜",
  age: 7,
};

const info1: Required<Props> = {
  name: "小杜杜",
  age: 7,
  sex: true,
};

Readonly

Readonly 语法Readonly<T> 作用:将所有属性都加上 readonly 修饰符来实现。也就是说无法修改

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

let info: Readonly<Props> = {
  name: "小杜杜",
  age: 7,
};

info.age = 1; //error read-only 只读属性

从上述代码上来看, Readonly修饰后,属性无法再次更改,智能使用

Record

Record 语法:Record<K extends keyof any, T>

作用:将 K 中所有的属性的值转化为 T 类型。

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

type InfoProps = "JS" | "TS";

const Info: Record<InfoProps, Props> = {
  JS: {
    name: "小杜杜",
    age: 7,
  },
  TS: {
    name: "TypeScript",
    age: 11,
  },
};

从上述代码上来看, InfoProps的属性分别包含Props的属性

需要注意的一点是:K extends keyof any其类型可以是:stringnumbersymbol

Pick

Pick 语法:Pick<T, K extends keyof T>

作用:将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。

interface Props {
  name: string;
  age: number;
  sex: boolean;
}

type nameProps = Pick<Props, "name" | "age">;

const info: nameProps = {
  name: "小杜杜",
  age: 7,
};

从上述代码上来看, Props原本属性包括nameagesex三个属性,通过 Pick我们吧nameage挑了出来,所以不需要sex属性

Exclude

Exclude 语法:Exclude<T, U>

作用:将 T 类型中的 U 类型剔除。

// 数字类型
type numProps = Exclude<1 | 2 | 3, 1 | 2>; // 3
type numProps1 = Exclude<1, 1 | 2>; // nerver
type numProps2 = Exclude<1, 1>; // nerver
type numProps3 = Exclude<1 | 2, 7>; // 1 2

// 字符串类型
type info = "name" | "age" | "sex";
type info1 = "name" | "age";
type infoProps = Exclude<info, info1>; //  "sex"

// 类型
type typeProps = Exclude<string | number | (() => void), Function>; // string | number

// 对象
type obj = { name: 1; sex: true };
type obj1 = { name: 1 };
type objProps = Exclude<obj, obj1>; // nerver

从上述代码上来看,我们比较了下类型上的,当 T 中有 U 就会剔除对应的属性,如果 U 中又的属性 T 中没有,或 T 和 U 刚好一样的情况都会返回 nerver,且对象永远返回nerver

Extra

Extra 语法:Extra<T, U>

作用:将 T 可分配给的类型中提取 U。与 Exclude相反

type numProps = Extract<1 | 2 | 3, 1 | 2>; // 1 | 2

Omit

Omit 语法:Omit<T, U>

作用:将已经声明的类型进行属性剔除获得新类型

image

Exclude的区别:Omit 返回的是新的类型,原理上是在 Exclude之上进行的,Exclude是根据自类型返回的

NonNullable

NonNullable 语法NonNullable<T> 作用:从 T 中排除 nullundefined image

ReturnType

ReturnType 语法ReturnType<T>

作用:用于获取 函数 T 的返回类型。

type Props = ReturnType<() => string>; // string
type Props1 = ReturnType<<T extends U, U extends number>() => T>; // number
type Props2 = ReturnType<any>; // any
type Props3 = ReturnType<never>; // any

从上述代码上来看, ReturnType 可以接受 any 和 never 类型,原因是这两个类型属于顶级类型,包含函数

Parameters

ParametersParameters<T> 作用:用于获取 获取函数类型的参数类型

    type Props = Parameters<() => string> // []
    type Props1 = Parameters<(data: string) => void> // [string]
    type Props2 = Parameters<any>; // unknown[]
    type Props3 = Parameters<never>; // never

End

参考:

小结

到此,有关TS的知识就已经说完了,相信掌握了这些知识,你一定会对TS有更深的理解,这篇文章按照自己的理解,进行分类,个人觉得这样的分类比较合理,如果有什么更好的建议,欢迎在评论区指出~

想到自己刚接触TS的时候,是有点抵触的,但随着时间的推移,发现TS真的很香,并且TS也不算是很难,只要你花费一定的时间,在结合与项目,你就会发现真香定律

相信这篇文章已经极大程度的解决了TS相关的代码,希望这篇文章能让你迅速掌握TS,喜欢的点个赞👍🏻支持下吧(● ̄(エ) ̄●)

作者:小杜杜 链接:https://juejin.cn/post/7088304364078497800 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

AnnGreen1 commented 4 months ago

Me: typescript函数重载是什么?为什么要声明多次?使用|多标注几种类型不就行了吗?

@GPT-3.5 Turbo: TypeScript函数重载是指在函数定义中声明多个函数签名,每个函数签名都有不同的参数类型或个数。当调用函数时,TypeScript编译器会根据传入参数的类型和个数来确定调用哪个函数签名。

使用函数重载的主要原因是为了提高代码的可读性和类型安全性。通过使用函数重载,可以明确指定函数的不同参数类型和返回值类型,使代码更易于理解和维护。此外,函数重载还可以帮助开发人员在编译时捕获潜在的类型错误,提高代码的质量。

虽然使用|操作符可以将多种类型标注在一个参数上,但是这种方式并不能完全替代函数重载。因为使用|操作符只能标注参数的类型,而无法标注参数的个数和返回值类型。而函数重载可以更精确地定义函数的多个版本,以确保函数在不同情况下都能正确地被调用。

因此,尽管在某些情况下使用|操作符可以简化代码,但在需要精确控制函数参数类型、个数和返回值类型时,函数重载仍然是一个更好的选择。

AnnGreen1 commented 4 months ago

Me: 请举例说明,并告诉我如何才是typescript中正确的重载,参数类型?返回值类型?参数数量?

@GPT-3.5 Turbo: 下面是一个简单的示例,演示了如何在TypeScript中正确地使用函数重载:

function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
  if (typeof a === 'number' && typeof b === 'number') {
    return a + b;
  } else if (typeof a === 'string' && typeof b === 'string') {
    return a + b;
  } else {
    throw new Error('Parameters must be of the same type');
  }
}

console.log(add(1, 2)); // 输出 3
console.log(add('Hello', 'World')); // 输出 HelloWorld

在上面的示例中,我们定义了一个名为add的函数,它有两个重载版本。第一个重载版本接受两个参数为数字类型,并返回一个数字类型的结果;第二个重载版本接受两个参数为字符串类型,并返回一个字符串类型的结果。最后,我们定义了一个实际的函数实现,根据传入参数的类型来执行不同的操作。

在正确使用函数重载时,需要注意以下几点:

  1. 每个重载版本的参数类型、参数数量和返回值类型必须严格匹配。
  2. 最后一个函数实现的参数类型可以是泛型any类型,用于处理未在重载中定义的情况。
  3. 函数实现的参数类型和返回值类型应该与重载版本中的参数类型和返回值类型兼容。

通过正确使用函数重载,可以提高代码的可读性和类型安全性,确保函数在不同情况下都能正确地被调用。

AnnGreen1 commented 4 months ago

Me: 参数类型和数量相同,返回值类型相同,这是重载吗?和C++相同吗?

@GPT-3.5 Turbo: 在 TypeScript 中,参数类型和数量相同、返回值类型相同的函数声明并不会被视为函数重载。这种情况下,TypeScript 会将这些函数声明视为同一个函数的不同实现,而不是函数重载。

与 TypeScript 不同,C++ 中的函数重载是指在同一作用域内可以定义多个具有相同函数名但参数类型、数量或顺序不同的函数。在 C++ 中,通过参数类型、数量或顺序的不同来区分同名函数,实现了函数重载的功能。

因此,虽然 TypeScript 和 C++ 都支持函数重载的概念,但在 TypeScript 中,参数类型和数量相同、返回值类型相同的函数声明并不会被视为函数重载,而在 C++ 中,函数重载是通过参数类型、数量或顺序的不同来区分同名函数的。

AnnGreen1 commented 4 months ago

Me: C++中,我一般会实现每一个重载函数,而在你给出的TypeScript重载示例中为什么只实现了最后一个函数,并且还在里面判断了类型?这样重载还有意义吗?

@GPT-3.5 Turbo: 在我给出的 TypeScript 示例中,只实现了最后一个函数,而没有单独实现每一个重载版本。这是因为在 TypeScript 中,函数重载的目的是为了提供多个函数签名,以便在编译阶段进行类型检查和函数匹配,而实际的函数实现可以集中在最后一个函数中。

在 TypeScript 中,当调用一个重载函数时,编译器会根据提供的参数类型和数量来匹配最合适的函数签名,然后调用最后一个函数实现。因此,在实际的函数实现中,我们可以使用类型检查来处理不同参数类型的情况,以确保函数在不同情况下都能正确地执行。

虽然在 TypeScript 中只实现最后一个函数可能看起来有些不同于在 C++ 中为每个重载函数单独实现的做法,但这种方式仍然是符合 TypeScript 的设计理念的。通过集中实现在最后一个函数中,并在其中进行类型检查,可以简化代码并确保函数的行为符合预期。

因此,尽管在 TypeScript 中的函数重载实现方式与 C++ 稍有不同,但仍然具有意义,可以提高代码的可读性和类型安全性。

AnnGreen1 commented 4 months ago

Me: 那在typescript中可以和C++中一样,单独实现每一种重载函数,而不去在每个函数内部在考虑类型的问题可以吗?这样种方法更推荐哪种呢?为什么?

@GPT-3.5 Turbo: 在 TypeScript 中,你也可以像在 C++ 中一样,单独实现每一种重载函数,而不在每个函数内部考虑类型的问题。这种方式在 TypeScript 中也是可行的,可以根据不同的参数类型分别实现不同的重载版本。

然而,需要注意的是,如果你选择单独实现每一种重载函数,那么在实现过程中需要确保每个重载函数的参数类型、数量和返回值类型与重载声明完全匹配,以避免类型不匹配的错误。另外,这种方式可能会导致代码冗余,尤其是在参数类型相似但不完全相同的情况下。

相比之下,在 TypeScript 中集中实现所有重载的函数实现并在其中进行类型检查的方式更为推荐。这种方式可以简化代码结构,减少重复代码,并确保函数的行为符合预期。通过集中实现函数实现,可以更好地维护和管理代码,并提高代码的可读性和可维护性。

因此,虽然在 TypeScript 中可以选择单独实现每一种重载函数,但更推荐的做法是集中实现所有重载的函数实现并在其中进行类型检查,以简化代码结构并确保代码的正确性。