头图

TypeScript 学习笔记

黄刀小五
English

Hello, 各位勇敢的小伙伴, 大家好, 我是你们的嘴强王者小五, 身体健康, 脑子没病.

本人有丰富的脱发技巧, 能让你一跃成为资深大咖.

一看就会一写就废是本人的主旨, 菜到抠脚是本人的特点, 卑微中透着一丝丝刚强, 傻人有傻福是对我最大的安慰.

欢迎来到小五随笔系列TypeScript 学习笔记.

导读

本文初衷为笔者在使用一段时间 TS 后, 对所学所想做一个记录. 文章内容较为基础, 适合作为入门级教程学习; 且涵盖内容并不全面, 缺失包含类在内的诸多内容. 如果各位看官不介意以上几点, 欢迎与笔者一同进入这严谨却又妙趣横生的奇幻旅途.

基础类型

Boolean - 布尔

let flag: boolean = false;

Number - 数字

let num: number = 10;

👺 扩展: number类型还支持 二进制(0b开头)八进制(0o开头)十六进制(0x开头) 字面量. 运算或输出时会转换为对应的十进制.

let num02: number = 0b1010;
let num08: number = 0o744;
let num16: number = 0xf00d;

String - 字符串

let str: string = '黄刀小五';

Array - 数组

数组共有两种定义方式: Array<T>T[] (T为该数组每项的元素类型)

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

Tuple - 元组

👺 特点: <数组>, <长度已知>, <每项元素类型不尽相同>

😼 就是一个给每项都定义数据类型的数组

let tuple: [string, string, number, boolean?] = ['No.1', '黄刀小五', 18];

🦥 应用:

  • React Hook 的 useState
import { useState } from 'react';
const [loading, setLoading] = useState<boolean>(false); // 这里类型可省略, 详见后文类型推断
  • csv 数据格式
type Touple = [string, number, string];
let csvData: Touple[] = [['黄刀小五', 18, '男'], ['二狗子', 14, '男']];

👺 扩展: 元组越界

let tuple: [number, string] = [1, '黄刀小五'];
tuple.push(2); // ✅ right 元组越界时, 该项类型相当于对元组的各项类型做联合

/* 过程如下, 看不懂的看官莫慌, 请先看后文 */
type UnionType<T> = T extends (infer P)[] ? P : never;
type TupleType = UnionType<typeof tuple>; // number | string

Enum - 枚举

enum Active {
  inactive,
  active,
}

enum Fruit {
  apple = 'apple',
  orange = 'orange',
  banana = 'banana',
}

若枚举类型未指定值或指定的值为number类型, 如上述 Active, 可对其进行双向取值: Active[0]Active['active'], 其映射如下【👇】

{
  0: 'inactive',
  1: 'active',
  inactive: 0,
  active: 1,
}

可对枚举的其中一项进行指定数值(通常为第一项), 其余项会顺序递增

enum Type {
  active = 1,
  inactive,
}

{ // 对应映射为
  1: 'active',
  2: 'inactive',
  active: 1,
  inactive: 2,
}

😼 tips: 建议采用赋值形式的枚举. 可读性更高, 如上述 Fruit

🦥 应用:

  • 结合 switch ... case 使用:
enum ActionType {
  ADD = 'ADD',
  EDIT = 'EDIT',
  DELETE = 'DELETE',
}

const reducer = (type: ActionType) => {
  switch(type) {
    case ActionType.ADD:
      // xxx
      break
    case ActionType.EDIT:
      // xxx
      break
    ...
  }
}

reducer(ActionType.ADD); // ✅ right
let params = 'ADD';
reducer(params); // ❎ error (类型“string”的参数不能赋给类型“ActionType”的参数)

/* 😼 tips: 非赋值形式大家可自行尝试 */
  • 定义类型映射 或 定义一组常量
enum Status { // 类型映射
  unprocessed = 1, // 未处理
  processed, // 已处理
  refused, // 已拒绝
}

enum Fruit { // 常量
  apple = '苹果',
  orange = '橘子',
  banana = '香蕉',
}

👺 扩展: keyof typeof Enum, 可将枚举类型转换为联合类型; 相信我, 你会用到的😏

enum ActionType {
  ADD,
  EDIT,
  DELETE,
}
type ActionTypeConst = keyof typeof ActionType // 'ADD' | 'EDIT' | 'DELETE'

Any - 任意类型

let value: any;

😼 tips: 食物链最顶端, 应尽量减少 any 的使用.

Unknown - 未知类型

let value: unknown;

unknown 表示未知类型, 与 any 不同的是无法对 unknown 类型执行任何操作. 仔细思考下 <any: 任意类型>、 <unknown: 未知类型> 的区别.

👉 以 numbertoFixed() 方法举例:

  • unknown 代表我不知道这是什么类型, 而只有 numbertoFixed() 方法, 故不能使用;
  • any 代表我可以是任意类型, 此时调用 toFixed() 方法的我就是 number 类型;

😼 tips: 如想使用 any, 请先考虑是否可用 unknown 代替. (搭配后文类型保护可安心食用)

Void - 无类型

常用于没有具体返回值的函数

const fn = (str: string): void => {
  // 执行xxx事件
}

Null 和 Undefined

let u: undefined = undefined;
let n: null = null;

这两个不知道要说点啥, 就补充以下两点吧 (然而与 TS 没啥关系)

  1. Number(undefined) => NaN, Number(null) => 0
  2. const fn = (arg?: string) => { ... } arg的类型是 string | undefined

Never - 永不返回

let n: never;
  • never 表示那些永远不存在的值, 当我们不想捕获当前值时, 可使用 never;
  • 任何类型都不可赋值给 never, 包括 any;
  • never | T = T 此特性可用来过滤掉不需要的值;

🦥 应用:

  • 做类型检查
type Type = string | number;

const fn = (arg: Type) => {
  if (typeof arg === 'string') { ... }
  else if (typeof arg === 'number') { ... }
  else { const check: never = arg; }
}

如上, 此时永远不会走到 else, 下面我们做以下操作:

- type Type = string | number;
+ type Type = string | number | boolean;

* 此时 else 中的 check 会报错 (不能将类型“boolean”分配给类型“never”)

类型推论

如果没有指定类型, TS 会根据类型推论推断出一个类型.

let val; // 推论成: let val: any;
let num = 10; // 推论成: let num: number = 10;
num = '黄刀小五'; // ❎ error (不能将类型“string”分配给类型“number”)

🦥 应用: <单一静态类型 - 如上述num><函数返回值 - 一般情况均可正确推断其返回值类型, 不用额外指定><循环中的子元素>

😼 tips: 如果 TS 能正确推断出其类型, 我们可采用类型推论而不必定义类型.

类型断言

类型断言用来告诉编译器 “我知道自己在干什么”, 有 尖括号as 两种写法. 在 $tsx$ 语法中, 只支持 as.

* 😼 tips: 下面我们使用数组来举个例子, 实际场景中应使用元组.

type Key = string | number;
let arr: Key[] = ['黄刀小五', 18];

// 不能将类型“Key”分配给类型“string”
- let name = arr[0];
- console.log(name.length);

// 使用类型断言
+ let name = arr[0] as string;
+ let name = <string>arr[0];
+ console.log(name.length);

Interface - 接口

接口用来定义对象的类型

interface User {
  readonly id: number; // 只读属性, 不可修改
  name: string; // 必须属性
  desc?: string; // 可选属性
  say?: (name: string) => void; // 方法
}

const say = (name: string) => { ... }
let user: User = {
  id: 1,
  name: '黄刀小五',
  say,
}

user.id = 2; // ❎ error (无法分配到 "id" ,因为它是只读属性)

索引签名 - 使接口更加灵活

interface User {
  [key: string]: string; // 表示 key 为 string, value 为 string 的任意属性
}

let user:User = {
  name: '黄刀小五',
  desc: '菜鸡前端一只',
}

😼 tips: 所有成员都必须符合索引签名的特征, 索引签名参数类型必须为 string | number

interface User {
  [key: string]: string;
  age: number; // ❎ error (类型“number”的属性“age”不能赋给字符串索引类型“string”。)
}

接口合并

interface User {
  name: string;
}
interface User {
  age: number;
}
/* 两者会合并为👇 */
interface User {
  name: string;
  age: number;
}

接口继承

关键字: extends

interface Person {
  name: string;
}
interface User {
  age: number;
}
interface Student extends Person, User {
  desc: string;
}
/* Student接口格式如下👇 */
interface Student {
  name: string;
  age: number;
  desc: string;
}

😼 tips: 接口和类型别名均可使用的情况下使用接口

🤔 思考: 如何定义一个树形结构

interface Tree {
  key: number;
  value: string;
  child?: Tree[];
}
let tree: Tree[] = [];

类型别名

顾名思义, 为该类型取一个新的名字

type Key = string | number;

与 Interface 对比

  • type 不支持继承和声明合并, interface 可以, 参考上文;
  • type 更为通用, 右侧可以是任意类型, interface 主要用于定义对象;
  • typeinterface 均可使用的情况下使用 interface;

联合类型与交叉类型

  • 联合类型 (A | B)
type Key = string | number; // 代表 string 或 number 类型

😼 tips: 联合类型可以用来声明具体的值

type Status = 'active' | 'inactive';
  • 交叉类型 (A & B)
interface User {
  name: string;
}
interface Student {
  age: number;
}
type Blogger = User & Student;

/* Blogger类型格式如下👇 */
{
  name: string;
  age: number;
}

😼 tips: 两个基础类型做交叉, 会生成 never 类型

type Key = string & number; // never, 没有类型可以满足即是 string 又是 number

类型查找

类型查找可以提取对象类型上某一属性的类型

interface Person {
  User: {
    name: string;
    age?: number;
  }
}
type User = Person['User']

😼 tips: 常用于第三方库类型无法引用的场合

常用关键字

const

👺 配合类型断言 as 来声明常量

type Status = 'active' | 'inactive';
const fn = (status: Status) => { ... }

- let status = 'active'; // 此时 'active' 被解析为字符串而非常量
- fn(status); // ❎ error (类型“string”的参数不能赋给类型“Status”的参数)

* 以下3种等价, 均可将 'active' 解析为常量
+ let status = 'active' as const;
+ const status = 'active';
+ let status: Status = 'active';
+ fn(status); // ✅ right

😼 tips: 第三方插件定义的常量经常需要配合 as const 使用呦

typeof

👺 用来获取变量的类型

let str = '黄刀小五';
type Str = typeof str; // type Str = string

let user = { name: '黄刀小五' }
type User = typeof user; // type User = { name: string; }

keyof

👺 用来提取对象类型的 key

interface User {
  name: string;
  age?: number;
}
type Key = keyof User; // type Key = 'name' | 'age'

类型保护

为什么需要类型保护

const fn = (value: string | number) => {
  if (value.length) { ... } // ❎ error (类型“string | number”上不存在属性“length”)
}

上述代码, 如果想在 value: string 时执行一段逻辑要怎么办呢?

此时就需要类型保护了, 使用类型保护后, 当前代码段会按照事先所指定的类型执行.

👺 typeof

const fn = (value: string | number) => {
  if (typeof value === 'string') { // 利用 typeof 限制 value 的类型为 string
    console.log(value.length);
  }
}

👺 is

const isString = (x: unknown): x is string => {
  return typeof x === 'string';
}
const fn = (value: string | number) => {
  if (isString(value)) console.log(value.length); // value is string
}

👺 in

表示某属性是否在当前对象中存在

interface User {
  name: string;
  age?: number;
}
const fn = (args: User) => {
  if ('age' in args) { ... }
  else { ... }
}

fn({ name: '黄刀小五', age: 18 }); // 执行 if 语句
fn({ name: '黄刀小五' }); // 执行 else 语句

泛型

👺 作用: 用于做代码复用

先来看个例子

const fn = (value: number): number => value;

🤔 思考 如果此时我想传入一个 string 类型并返回一个 string 类型呢

const fn = <T>(value: T): T => value;
fn<string>('黄刀小五'); // const fn: <string>(value: string) => string

* 😼 tips: 可根据 类型推断 推断出其为 string 类型, 而不用特意指定 -> `fn('黄刀小五');`

以上, 一个泛型就实现好了, 其可以将类型作为一个参数, 在调用时传入类型进行指定.

👺 注意: 在 $tsx$ 中, <T> 会被解析成标签, 可使用下面的写法:

const fn = <T extends {}>(value: T): T => value; // extends也可用于缩小T的范围
const fn = <T,>(value: T): T => value;
  • 传入多个类型:
const fn = <T, U>(type: T, value: U): U => { ... };
fn<boolean, string>(true, '黄刀小五');

泛型的作用相当之广, 如定义一个接口:

interface Teacher<T> {
  readonly id: number;
  name: string;
  student?: T[];
}
interface Student {
  readonly id: number;
  name: string;
  age?: number;
}
let teahcer: Teacher<Student> = {
  id: 1,
  name: '黄刀小五',
  student: [{
    id: 1001,
    name: '二狗子',
    age: 14,
  }]
}

常用语法糖

下面我们来用上述知识实现下 TS 中封装好的语法糖 (温馨提示: 建议先搞懂上文, 至少搞懂泛型在阅读下面内容) look down 👇

Readonly

👺 将对象类型的属性均变为只读

  • 代码实现
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};
  • Demo
interface Person {
  readonly name: string;
  age: number;
  desc?: string;
}
let person: Readonly<Person> = {
  name: '黄刀小五',
  age: 18,
}
person.age = 19; // ❎ error (无法分配到 "age" ,因为它是只读属性)

Partial

👺 将对象类型的属性均变为可选

  • 代码实现
type Partial<T> = {
  [K in keyof T]?: T[K];
};
  • Demo
interface Person {
  readonly name: string;
  age: number;
  desc?: string;
}
let person: Partial<Person> = {}; // 此时所有属性均为可选

Required

👺 将对象类型的属性均变为必须

  • 代码实现
type Required<T> = {
  [K in keyof T]-?: T[K];
};

😼 tips: -? 表示去掉可选符号 ?, 此符号 (-) 同样可用于其它位置, 如: -readonly 可去掉只读属性等.

  • Demo
interface Person {
  readonly name: string;
  age: number;
  desc?: string;
}
let person: Required<Person>> = {
  name: '黄刀小五',
  age: 18,
  desc: '基于搜索引擎的复制粘贴工程狮',
}; // 此时所有属性均为必须

Record

👺 将一个类型的所有属性值映射到另一个类型上并创建一个新的类型

  • 代码实现
type Record<K extends string, T> = {
  [P in K]: T;
};
  • Demo
interface Person {
  readonly name: string;
  age: number;
  desc?: string;
}
type Kind = 'teacher' | 'student';
let person: Record<Kind, Person> = {
  teacher: {
    name: '黄刀小五',
    age: 18,
  },
  student: {
    name: '二狗子',
    age: 14,
  }
}; // 将 Person 类型映射到 Kind 类型中

/* Record<Kind, Person> 相当于👇 */
type NewPerson = {
  [key in Kind]: Person
};

Extract

👺 从 T 中提取 U

  • 代码实现
type Extract<T, U> = T extends U ? T : never;
  • Demo
type Student = '二狗子' | '如花';
type Teacher = '黄刀小五' | '二狗子';
type Trainee = Extract<Student, Teacher>; // 输出 '二狗子'

/* Extract<Student, Teacher> 相当于👇 */
'二狗子' in Teacher -> '二狗子'
'如花' in Teacher -> never
'二狗子' | never = '二狗子'

Exclude

👺 从 T 中排除 U

  • 代码实现
type Exclude<T, U> = T extends U ? never : T;
  • Demo
type Student = '二狗子' | '如花';
type Teacher = '黄刀小五' | '二狗子';
type OnlyStudent = Exclude<Student, Teacher>; // 输出 '如花'

/* Exclude<Student, Teacher> 相当于👇 */
'二狗子' in Teacher -> never
'如花' in Teacher -> '如花'
'如花' | never = '如花'

Pick

👺 挑选对象中的部分属性

  • 代码实现
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}
  • Demo
interface Person {
  readonly name: string;
  age: number;
  desc?: string;
}
let person: Pick<Person, 'age' | 'desc'> = {
  age: 18,
};

/* Pick<Person, 'age' | 'desc'> 相当于👇 */
interface Person {
  age: number;
  desc?: string;
}

Omit

👺 忽略对象中的部分属性

  • 代码实现
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
  • Demo
interface Person {
  readonly name: string;
  age: number;
  desc?: string;
}
let person: Omit<Person, 'name'> = {
  age: 18,
};

/* Omit<Person, 'name'>相当于👇 */
interface Person {
  age: number;
  desc?: string;
}

ReturnType

👺 获取方法的返回值类型

  • 代码实现
type ReturnType<T> = T extends (
  ...args: any[]
) => infer R ? R : any;

-> infer 配合 extends 使用, 用于推断函数的返回值类型

  • Demo
const getName = (name: string) => name;
type ReturnGetName = ReturnType<typeof getName>; // string

Parameters

👺 获取方法的参数类型

  • 代码实现
type Parameters<T> = T extends (
  ...args: infer P
) => any ? P : never;

-> 这里 infer 用于推断函数的参数类型, Parameters 返回格式为元组.

  • Demo
const getName = (name: string) => name;
type ParamGetName = Parameters<typeof getName>; // [name: string]

NonNullable

👺 排除 nullundefined 类型

  • 代码实现
type NonNullable<T> = T extends null | undefined ? never : T;
  • Demo
type NewPerson = Person | null;
let person: NonNullable<NewPerson> = null; // ❎ error (不能将类型“null”分配给类型“Person”)

结束语

结合文章内容, 大家可根据实际需求封装更多的语法糖, 便于在项目中使用. 若想更细致的学习 TS, 这里推荐一个博主 阿宝哥, 其发表了很多关于 TS 的文章, 并针对 TS 的某一特征做了详细的讲解.

参考🔗链接

【TypeScript 文档】

【FESKY】 结合实例学习 Typescript

【刘哇勇】 TypeScript infer 关键字

阅读 16.6k

前端开发攻城狮, 擅长搬砖, 精通ctrl + c & ctrl + v, 日常bug缔造者.

277 声望
17 粉丝
0 条评论
你知道吗?

前端开发攻城狮, 擅长搬砖, 精通ctrl + c & ctrl + v, 日常bug缔造者.

277 声望
17 粉丝
宣传栏