likes
comments
collection
share

✨TypeScript与高级类型

作者站长头像
站长
· 阅读数 11

前言

这是“TS入门小记”栏目中的第二篇系列文章,文章内容为本人学习TS时记录的笔记和实际应用中的总结提炼而得。如果有错误的地方,大家可以在评论区中提出,我会以最快的时间进行更正🤞 上一篇文章:🤔TS中有哪些简单特性?


类型别名type

现在我们有这样一个代码,如果要再声明一个同样类型的对象,我们需要再重复声明一次类型。

我们应该尽可能复用我们的代码。

let man: {
  readonly name: string;
  age: number;
  retire: (date: Date) => void;
} = {
  age: 22,
  name: "kevin",
  retire: (date: Date) => {
    console.log(date);
  },
};

所以我们使用了type关键字

type Man = {
  readonly name: string;
  age: number;
  retire: (date: Date) => void;
};

let man: Man = {
  age: 22,
  name: "kevin",
  retire: (date: Date) => {
    console.log(date);
  },
};

当然,你也可以使用接口interface关键字,这个关键字我们会在后面讲。

联合类型|

我们可以使用联合类型,来声明一个变量的类型为多个类型的子集。

使用|来声明联合类型:

✨TypeScript与高级类型

但是,这时候,我们发现使用ts的代码补全时,只有number和string类型的共同的方法。

所以,我们要想办法将类型范围“缩小”:

✨TypeScript与高级类型

可以看到:现在的代码补全就是对应类型的方法了

我们再查看编译出的js代码:

"use strict";
const func = (param) => {
    if (typeof param === "number")
        return param;
    else
        return parseInt(param) * 1.2;
};

可以看到,我们所以联合类型只是ts编译器对变量进行的类型检查

交叉类型&

我们可以使用&来表示同时满足多个类型,也许你会想到这样声明一行代码:

let number_string: number & string;

但这是不合理的,因为没有值能够既是number类型又是string类型

我们通常用它来声明一个类型:

type Draggable = {
  drag: () => void;
};

type Resizable = {
  resize: () => void;
};

type UIWidget = Draggable & Resizable;

let textBox: UIWidget = {
  drag: () => {},
  resize: () => {},
};

字面量类型

假设我们有一个变量为quantity

let quantity: number;

这个变量的类型为number,也就是说我们可以赋值任何数值给这个变量。

但也许我们就只想给它赋值固定的数值?

ts允许我们使用字面量来声明类型,如string,number,boolean类型的值

let quantity: 50 = 100;//报错:不能将类型“100”分配给类型“50”。ts(2322)

或许你觉得这没有什么用,但当我们使用联合类型时,或许就有用了:

let quantity: 50 | 100 = 100;

当然,我们应该这样写:

type Quantity = 50 | 100;
let quantity: Quantity = 100;

对于字符串和布尔类型:

type Metric = "cm" | "m";
type Bools = true | false;

null类型

只有null为null类型,只有undefined为undefined类型

假如我们有一个函数:

const greet = (name: string) => {
  console.log(name.toUpperCase());
};
greet(null);//报错:类型“null”的参数不能赋给类型“string”的参数。ts(2345)

如果你想去除这个报错,你可以(不推荐)在tscofig.json中找到"strictNullChecks": true,打开并该为false

或许你真的想传入null类型,你可以声明联合类型:

当然,这里你不能光声明类型,因为在你使用name.toUpperCase()时它会报错:name可能为null。

const greet = (name: string | null | undefined) => {
  if (name) console.log(name.toUpperCase());
  else console.log("hello");
};
greet(null);
greet(undefined);

可选链/可选属性访问符?

现在假定我们有一个函数,用来输出客户的生日,或许你不知道客户的名字。我们将name的类型定义为与null和undefined联合的类型

type Customer = {
  birthday: Date;
};

function getCustomer(id: number): Customer | null | undefined {
  return id === 0 ? null : { birthday: new Date() };
}

let customer = getCustomer(0);
if (customer !== null && customer !== undefined) console.log(customer.birthday);

在ts中,我们可以通过可选链/可选属性访问符(在.前使用),当这个方法/属性被定义时我们调用,如果为null或者undefinedts会返回undefined,并且有短路运算的特性。

type Customer = {
  birthday: Date;
};

function getCustomer(id: number): Customer | null | undefined {
  return id === 0 ? null : { birthday: new Date() };
}

let customer = getCustomer(0);
console.log(customer?.birthday);//undefined

现在我们让Customer类型中的birthday属性为可选,然后链式调用getFullYear方法。注意加上可选属性访问符。因为这时的customer?.birthday可能为null或者undefined:

type Customer = {
  birthday?: Date;
};

function getCustomer(id: number): Customer | null | undefined {
  return id === 0 ? null : { birthday: new Date() };
}

let customer = getCustomer(0);
console.log(customer?.birthday?.getFullYear());//注意getFullYear()前的可选属性访问符

同时,我们也可以在数组和调用方法中使用:

function tryGetFirstElement<T>(arr?: T[]) {
  return arr?.[0];
  // equivalent to
  //   return (arr === null || arr === undefined) ?
  //       undefined :
  //       arr[0];
}
async function makeRequest(url: string, log?: (msg: string) => void) {
  log?.(`Request started at ${new Date().toISOString()}`);
  // roughly equivalent to
  //   if (log != null) {
  //       log(`Request started at ${new Date().toISOString()}`);
  //   }

  const result = (await fetch(url)).json();

  log?.(`Request finished at at ${new Date().toISOString()}`);

  return result;
}

无效合并/Nullish Coalescing??

假如我们有这样的代码:

let speed: number | null = null;
let ride = {
  speed: speed ? speed !== null : 30,
};

在ts中,我们可以使用无效合并来简化代码:

let speed: number | null = null;
let ride = {
  speed: speed ?? 30,
};

当处理 null 或者 undefined 时,它可以作为一种「倒退」到默认值的方式

类型断言

有些时候,我们比ts编译器更清楚变量/常量的类型

let phone = document.getElementById("phone");//let phone: HTMLElement | null

假设我们真的有一个元素的id为phone,那么变量phone的类型就应该为HTMLElement而不是HTMLElement | null

并且前面的经验也告诉我们这不利于代码补全。

我们可以使用as关键字

let phone = document.getElementById("phone") as HTMLElement;

并且这时候的代码推断的提示也是HTMLElement类型的方法。

当然,我们也可以使用尖括号来进行断言<>

let phone = <HTMLElement>document.getElementById("phone");

unknown比any更安全

之前我们已经讲过,any虽然会让我们避免报错,但大量的使用any会让我们失去使用ts的意义。同样的ts也不鼓励我们使用any,而是推荐我们使用unknown,意思是”不知道是什么类型“。

不过我们直接使用unknown时会报错:

function reder(document: unknown) {
  document.toUpperCase();//报错:对象的类型为 "unknown"。ts(2571)
}

这是 unknown 类型的主要价值主张:TypeScript 不允许我们对类型为 unknown 的值执行任意操作。相反,我们必须首先执行某种类型检查以缩小我们正在使用的值的类型范围。

像前面提到的那样,我们进行类型范围的缩小:

function reder(document: unknown) {
  if (typeof document === "string") document.toUpperCase();
}

但typeof只能用于基本类型,而像自定义类型,我们需要用到instanceof关键字

class Behavior {
  move() {}
}

function reder(document: unknown) {
  if (document instanceof Behavior) document.move();
}

never类型

never代表永远不会发生的类型。它是 TypeScript 中的底层类型。它自然被分配的一些例子:

  • 一个从来不会有返回值的函数(如:如果函数内含有 while(true) {});
  • 一个总是会抛出错误的函数(如:function foo() { throw new Error('Not Implemented') }foo 的返回类型是 never);

例如:

function processEvents(): never {
  while (true) {}
}

processEvents();
console.log("never do");//这行代码在vscode中会呈灰色,如果不使用never类型,这会看上去能够执行

我们还可以在tsconfig.json中找到"allowUnreachableCode": true, 打开并改为false这表示我们不允许不能到达的代码

现在我们得到了一个提示,这对我们是很有用的:

✨TypeScript与高级类型

这也是为什么我们要使用never。

同样的:

let bar: (err: string) => never = (err: string) => {
  throw new Error(err + ":Throw my hands in the air like I just dont care");
};
bar("...");
console.log("never do"); //检测到无法访问的代码。ts(7027)

如果对这个系列的其他文章感兴趣,可以在我的专栏中的“TS入门小记”中找到其他文章🤞 ✨TypeScript与高级类型 目录:

(一)🧐为什么需要TS以及如何配置与debug?

(二)🤔TS中有哪些简单特性?

(三)✨TS与高级类型

(四)TypeScript与面向对象

(五)TypeScript中的难点:<泛型>

(六)设计模式与TypeScript装饰器(Decorator)