🚀🚀🚀Typescript通关秘籍(四)🔥🔥🔥

Type与Interface

上述使用的类型基本都是 JSTS 帮我们定义好的,或者说是内置的基础类型。

但是,它们都比较单一,光它们可不能满足我们实际的业务场景,我们可能还要一些更加复杂的类型。

TS 给开发者提供了两种创建复杂类型的方式:typeinterface

Type

type 修饰符用来定义一个类型的别名

javascript 复制代码
type Name = string;
type Age = number;
type Obj = {
  name: string;
  age: number;
};

let name: Name = '橙某人';
let age: Age = 18;
let obj: Obj = {
  name: '橙某人',
  age: 18,
}

值类型

TS 中定义类型的时候,需要开发者时刻注意 "类型" 和 "值类型" 两者不一样的。

javascript 复制代码
type Name = '橙某人';
type Age = 18;
type Obj = {
  name: '橙某人',
  age: 18,
}

let nam1: Name = '橙某人'; // ✅
let nam2: Name = '橙某人1111'; // ❌

let age1: Age = 18; // ✅
let age2: Age = 19; // ❌

typeof

JS 中,typeof 能帮我们获取八种数据类型。

javascript 复制代码
console.log(typeof 1); // number
console.log(typeof '1'); // string
console.log(typeof true); // boolean
console.log(typeof null); // object
console.log(typeof undefined); // undefined
console.log(typeof {}); // object
console.log(typeof Symbol()); // symbol
console.log(typeof 1n); // bigint

而在 TS 中,typeof 能帮我们获取值的类型

javascript 复制代码
let name = '橙某人';
let age = 18;
let obj = {
  name: '橙某人',
  age: 18,
};

type Name = typeof name;
type Age = typeof age;
type Obj = typeof obj;

等价于

javascript 复制代码
type Name = string;
type Age = number;
type Obj = {
  name: string;
  age: number;
};

typeof 经常被我们用来获取对象的类型,因为对象属性一般比较多,或者是嵌套对象,这样让定义对象类型成了一个繁琐的事情,使用 typeof 能很好的解决这个问题。

Interface

interface 中文翻译为 "接口",它是声明对象类型的另一种方式。

javascript 复制代码
interface Obj {
  name: string; // 也可以是逗号分隔
  age: number;
}

let obj: Obj = {
  name: '橙某人',
  age: 18
}

let name: Obj.name = '橙某人';

type Age = Obj.age;

可选参数

javascript 复制代码
interface Obj {
  name: string;
  age?: number
}

let obj: Obj = {
  name: '橙某人'
}

对象方法

javascript 复制代码
interface Obj {
  fn: (a: number) => number;
}

interface Obj {
  fn(a: number): number
}

interface Obj {
  fn: { (a: number): number };
}

以上三种都是等价的,小编推荐使用第一种。😗

函数

我们知道函数也属性对象的一样,所以 interface 也可以用来声明独立的函数。

javascript 复制代码
interface Fn {
  (a: number, b: number): number
}

const fn: Fn = (a, b) => a + b;

继承

继承是接口的一个最重要特性,interface 可以通过 extends 关键字去继承其他类型,包括 interfacetypeclass 等都可以被继承。

继承 interface

javascript 复制代码
interface Person {
  name: string;
}
interface Student extends Person {
  age: number;
}

let s1: Student = {
  name: '橙某人',
  age: 18,
}

继承 type

javascript 复制代码
type Person = {
  name: string;
}
interface Student extends Person {
  age: number;
}

let s1: Student = {
  name: '橙某人',
  age: 18,
}

继承 class

javascript 复制代码
class Person {
  name: string = '';
  say() {

  }
}
interface Student extends Person {
  age: number;
}

let s1: Student = {
  name: '橙某人',
  age: 18,
  say: () => {}
}

能正常继承 classpublic 属性和方法,其他修饰符的属性和方法无法继承,或者说是没有意义。

interface 还允许多重继承,可以是继承多个 interface 或者是混合继承。

javascript 复制代码
interface Person1 {}
interface Person2 {}
type Person3 = {}
class Person4 {}

interface Student extends Person1, Person2, Person3, Person4 {}

但是需要注意❗父类型不能有同名的属性。

重名合并

多个重名的 interface 会进行合并。

javascript 复制代码
interface Obj {
  name: string
}
interface Obj {
  age: number;
}

let obj: Obj = {
  name: '橙某人',
  age: 18
}
  • 重名合并时,同一个属性类型不能冲突。
  • 重名合并时,同一个属性的函数会发生函数重载。

两者区别

  1. 继承:interface 使用 extends 关键字能实现继承,但 type 只能变向通过 & 符号来实现继承。

  2. 范围:type 能规范基本类型,联合类型、元组,但 interface 不行。

  3. 合并:interface 可以重复进行定义,会自动进行合并,但 type 不行。

  4. typeof:type 能利用 typeof 获取实例类型,进行 type 定义,interface 不行。

🍊关于如何获取类型别名或者接口中的某个子类型?

可以使用 [] 方括号。

typescript 复制代码
interface Person {
  name: string;
  age: string;
  sex: string;
}

type P = Person['name']; // string
let p: Person['name']; // string

联合类型与交叉类型

联合类型与交叉类型就像 JS 中的"或"与"且"一样,它们由两个或者多个其他类型组成的类型。

联合类型:|

交叉类型:&

或:||

且:&&

联合类型

联合类型(union types)表示可能是其中任何一种类型的值。

javascript 复制代码
let articleName: string | number;

// 类型与值类型混合
let age: number | '男' | '女';

// 类型别名
type Person1 = {
  name: string;
}
type Person2 = {
  age: number;
}
type Person = Person1 | Person2;

// 接口
interface P1 { }
interface P2 { }
interface P {
  name: P1 | P2;
  age: number | string
}

// 对象
let obj: {
  name: string;
  age: string | number;
}

// 函数
function fn(a: string | number): void | number { }

// 数组
let arr: Array<number | string>;
arr = [1, 2];
arr = ['a', 'b'];
let arr1: (number | string)[];

类型缩小/类型守护

联合类型本身其实就是将"类型放大"(type widening),但是这在具体逻辑使用的时候就犯难了。

比如:

javascript 复制代码
function fn1(a: number) {
  a.toFixed // ✅
}
function fn2(a: number | string) {
  a.toFixed // ❌
}

遇上这种情况,或者说凡是遇到联合类型的情况,在使用之前,我们都需要先进行"类型缩小"(type narrowing)。

javascript 复制代码
function fn2(a: number | string) {
  if(typeof a === 'number') {
    a.toFixed
  }
}

类型缩小的方式很多种。

javascript 复制代码
interface P1 {
  name: string;
}
interface P2 {
  age: number;
}
function fn(obj: P1 | P2) {
  if ('name' in obj) {
    console.log(obj.name);
  }
}

类型缩小有时也被称为类型守护

交叉类型

交叉类型(intersection types)表示必须满足其中任何一种类型的值。

上述的联合类型示例,都可将 | 符号换成 & 符号,让其变成交叉类型。

尽管这会让很多类型变得无意义,如:

javascript 复制代码
let articleName: string & number;

我们无法定义出一个值既是 string 类型,又是 number 类型,为此 articleName 的类型将变成一个 never 类型。

当然,这并不是说交叉类型就没有用了,它的主要用处还是在对象类型上。

type 利用交叉类型进行合并行为:

javascript 复制代码
type P1 = {
  name: string;
}
type P2 = {
  age: number;
}

let obj: P1 & P2 = {
  name: '橙某人',
  age: 18
}

不过,如果遇上同名属性,它的合并行为就和 interface 的自动合并行为有点不一样了, interface 是覆盖,而交叉类型则是取交叉部分。

javascript 复制代码
type P1 = {
  name: string;
}
type P2 = {
  name: string | number;
}

let obj1: P1 & P2 = {
  name: '橙某人', // ✅
}
let obj2: P1 & P2 = {
  name: 1, // ❌ name 只能是交叉的 string类型
}

类型断言

类型断言 这玩意又是啥?😮

就一句话,它能帮我们强制通知 TS "xx 它就是这个类型了,不需要你再帮我判定类型了,给我走"。

类型断言有两种语法形式:value as Type 或者 <Type>value

as

我们用上面"联合类型-类型缩小"的例子来看看。

javascript 复制代码
function fn2(a: number | string) {
  (a as number).toFixed
}

我们可以直接将 a 断言成一个 number 类型,那么它就能直接调用 toFixed 方法。

是不是很简单😋

<>

改成尖括号语法,如下:

javascript 复制代码
function fn2(a: number | string) {
  (<number>a).toFixed
}

这也没什么好说的,用就完事了,因为很多时候,我们可能拥有 TS 不能知道的值的类型信息,我们可以直接手动强制断言它。

多重断言

断言并不能是随意的,要有理有据,不能胡说八道喔。

如上面例子,由于是联合类型,所以我们断言成联合类型中的其中一个类型是比较合理的说法。

但是,如下:

javascript 复制代码
let str = '橙某人';
let num: number = str as number; // ❌

这情况就不行了,很明确的事实摆着呢,你不能欺骗 TS,要不它得哭了。

类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。

例如,这样子是可以的。

javascript 复制代码
let str = '橙某人';
let num: String = str as String; // ✅

但是却不能满足我们的需求。👊 不慌,这个时候就轮到多重断言出场了。

javascript 复制代码
let str = '橙某人';

let num1: number = str as any as number; // ✅
let num2: number = str as unknown as number; // ✅

既然能断言成父类型就简单了,我们可以直接断言成顶层类型再转成子类型。

顶层类型有 anyunknown 类型,它们兼容所有子类型。

(断言真有点像在胡说八道,搁这骗 TS 呢。😜)

as const断言

as const 断言可以用来修改类型推断的行为,但它只能用在字面量、枚举身上,不能用于变量。

当我们使用类型推断的时候,letconst 的行为是不一样的。

javascript 复制代码
let str = '橙某人'; // string
const Str = '橙某人'; // '橙某人'

let 定义的变量会推断出 string 类型。
const 定义的变量则会推断出 橙某人 值类型。

而当我们使用 as const 断言的时候,可以将 let 的类型推断行为修改成和 const 类型推断的行为一样。

javascript 复制代码
let str = '橙某人' as const; // '橙某人'
// or
let str = <const>'橙某人'; // '橙某人'

使用 as const 去断言 let 声明的变量后,该变量也具备 const 常量的性质,不可被修改。

javascript 复制代码
let str = '橙某人' as const;

str = 'a'; // ❌

字面量对象

字面量对象整个对象或者单个属性都可以使用 as const 断言,且两者表现行为不一样,整个对象使用会使所有属性全部会变成只读状态。

js 复制代码
let obj1 = {
  name: '橙某人',
  age: 18,
}; // obj类型: { name: string, age: number }

let obj2 = {
  name: '橙某人' as const,
  age: 18,
}; // obj类型: { name: '橙某人', age: number }

let obj3 = {
  name: '橙某人',
  age: 18,
} as const; // obj类型: { readonly name: '橙某人', readonly age: 18 }

obj3.name = 'a'; // ❌

字面量数组

字面量数组使用 as const 断言后,类型推断会将其推断成一个只读的元组。

javascript 复制代码
let arr1 = [1, 2, 3]; // number[]
arr1.push(4);

let arr2 = [1, 2, 3] as const; // readonly [1, 2, 3]
arr2.push(4);// ❌

const arr3 = [1, 2, 3]; // number[]
arr3.push(4);
arr3 = []; // ❌

const arr4 = [1, 2, 3] as const; // readonly [1, 2, 3]
arr4.push(4); // ❌

枚举

枚举的成员也可以使用 as const 断言。

javascript 复制代码
enum Color {
  red, 
  green,
  blue,
}

let red = Color.red; // Color
let red1 = Color.red as const; // Color.red

应用

说了半天,那这鸡肋玩意能干嘛呢?😁

呃...小编用了几年 TS,好像自己也还没用过。。。

网上看了一圈,有个不错的应用,就是利用字面量数组使用 as const 断言后能变成一个元组的特性。

javascript 复制代码
function add(a: number, b: number) {
  return a + b;
}

const nums = [1, 2];
add(...nums); // ❌

const nums = [1, 2] as const;
add(...nums); // ✅

非空断言(!)

非空断言(Non-null assertion operator)是一种类型断言的特殊形式,用于告诉编辑器一个变量不为 nullundefined

具体什么意思呢,来看例子:

javascript 复制代码
let nickname: string | undefined | null;
let realname: string;

function fn() {
  nickname = '橙某人';
}
fn();

realname = nickname; // ❌

其实我们能很明确知道 nickname 是会有值的,而且刚好是 string ,但是编辑器却不知道,这种时候就可以使用非空断言。

javascript 复制代码
let nickname: string | undefined | null;
let realname: string;

function fn() {
  nickname = '橙某人';
}
fn();

realname = nickname!; // ✅

至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。

老样子,点赞+评论=你会了,收藏=你精通了。

相关推荐
吕彬-前端15 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱17 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai27 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking1 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb