TypeScript 进阶手册

本文旨在帮助更高效使用 TypeScript、帮助我们成为 TypeScript 高手,无论在面试、指导新人、交流中都可以一展所长,限于内容较多、会分2篇发布。

我对 TypeScript 的态度

关于是否推荐使用 TypeScript、一直各有声音,很多开发者也各有所好,有人热爱 TypeScript 的严谨和专业,有人更偏爱 JavaScript 本身的灵活与简洁。打开网络、你可能在某一刻接收到《使用 TypeScript 的 10 个 理由 》信息,正当你想在项目中大显身手的时候、搜索引擎给你推荐《不推荐使用 TypeScript 的 7 个 理由 》。这些不是本文要讨论的话题,我的建议是在实践中形成自己的经验和想法 :"If you can't figure out it,just try and do it. Hesitating or waiting for too long is a waste of time. "

建议学习TypeScript、用好TypeScript,但谨记它不是万能的、也并不是始终适用,可以把它当做一种开发 JavaScript 应用的语法、风格,代码最重要的还是产生价值,应用开发效率、代码可读性和高拓展性至关重要,切勿舍本求末。

TypeScript 的特点

强类型

TypeScript是一门强类型语言、类型规则是严格执行的,它的优势是:

  1. 有助于更早发现不稳定性问题:如对某变量操作使用不当、在 string 上使用number 相关操作、在未来的某一刻很可能暴雷;
  2. 更有利于多人或跨组件协作:通过智能提示、代码检测工具能更有效地进行代码推断、提高代码质量和协作效率,如我们可以更快捷的使用第三方库、清晰的知道 API 需要的数据格式、类型,也无需手工 check 是否遗漏关键信息。

编程范式

支持函数式和面向对象的编程方式写代码,在JavaScript 的基础上更强调面向对象特性,如继承、类、可见性范围,命名空间等,有更完整的代码组织能力。

  1. 面向对象编程(Object Oriented Programming) :TypeScript 提供了丰富的面向对象编程特性,包括类、接口、泛型等。可以极大的提升代码组织和复用性、在常见的代码迁移/重构场景优势明显;
  2. 函数式编程(Functional Programming) :函数式编程将函数作为一等公民,可以实现高度的抽象和复用;

语法

TypeScript 是JavaScript 的超集,继承了JavaScript的全部语法,所有JavaScript 代码都可以作为TypeScript 代码运行;除此之外,TypeScript 也为JavaScript 增加了对ES6的支持。自然而然、在享受其益处的同时、我们会需要一定的学习和上手成本,但是这一定是值得的。

类型世界

合理使用type 与 interface

type(类型别名) 和 interface 都可以用来定义类型、且多数情况可以互换,但他们有没有区别、以及如何更好的使用他们呢?推荐的方式是:使用interface 描述对象、类的结构 ,用type描述类型别名、联合类型、交叉类型等复杂类型

An interface can be named in an extends or implements clause, but a type alias for an object type literal cannot.

An interface can have multiple merged declarations, but a type alias for an object type literal cannot.

相同点

  • 两者都可以描述一个对象或者函数;
  • 都支持拓展(extends),语法不一样;
typescript 复制代码
type TBasicInfo = {
	name: string;
}
type TUser = TBasicInfo & { religion: string  };

interface IUser extends TBasicInfo { 
  religion: string; 
}

差异点

  • type 可以声明基本类型别名,联合类型,元组等类型;
  • interface 能够声明合并,支持实现(implements);
typescript 复制代码
type StringOrNumber = string | number;
typescript 复制代码
interface User {
  name: string;
}

interface User {
  age: string;
}

// Last output =
interface User {
  name: string;
  age: number;
}

善用枚举

枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。

枚举可以让代码拥有更好的类型提示,同时可以把常量真正地约束在一个命名空间下,最常见的场景就是项目中常量声明。

javascript 复制代码
const RouteMap = {
  HomePage: "xx",
  SettingPage: "xxxx",
}
// =>
enum RouteMap {
  HomePage: "xx";
  SettingPage: "xxxx";
}

// Usage
RouteMap.HomePage;

拒绝AnyScript - 理解any 、unknown 与 never

any 、unknown 与 never 都是 TypeScript 的内置类型,有不同的含义和使用场景。

any:表示任意类型 ,能兼容所有类型,也能够被所有类型兼容,使用 any 时类型推导与检查不会生效;

typescript 复制代码
let obj: any = null;

// 以下代码均无异常提示
obj.getCoordinate();
obj.x.y;

unknown:表示类型未知、但未来会确定类型。

typescript 复制代码
let obj: unknown = null;

obj.getCoordinate(); // 报错:obj类型为 unknown

(obj as { getCoordinate: () => string  }).foo(); // ok

never:代表无类型,不带类型信息,常见的是 throwerror 函数。

typescript 复制代码
function throwError(): never {
  throw new Error()
}

有时候也可以用它来做流程分支运行时安全/错误检查、兜底:

typescript 复制代码
function  handle(key: string | number): boolean {
  if (typeof key === 'string') {
    return true;
  } else if (typeof key === 'number') {
    return false;
  }

  // 如果不是never 类型会报错:因为检查到无法访问的代码 / 无预期返回类型
  // throwError 函数返回为never 类型、则可以被调用
  return throwError('Unexpected error');
}

function throwError(message: string): never {
  throw new Error(message);
}

Class 的隐秘用法

修饰符

Class 的基本结构主要有构造函数、属性、方法和访问符,我们主要介绍给 类成员添加的访问性修饰符:public / private / protected,不显式使用访问性修饰符,默认会被标记为 public:

  • public:此类成员在类、类实例、子类中均能被访问;
  • private:此类成员仅支持在类内部访问;
  • protected:此类成员仅支持在类与子类中被访问。
typescript 复制代码
// 在构造函数中对参数设置访问性修饰符,等价于 单独声明类属性然后在构造函数中进行一一赋值
class User {
  constructor(public name: string, private age: number) { }
}

new User('Didi', 22);

static 私有构造函数

当类的构造函数标记为private 后,就只允许在类内部访问了,主要作用场景是定义工具库、工具库内都是静态方法,并不期望被实例化:

typescript 复制代码
class Utils {
  public static tactics = ['money', 'number', 'ratio'];
  
  private constructor(){}

  public static transferNum() {
  }
}

Utils.transferNum();

泛型

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

泛型的本质是为了参数化类型、即不创建新类型,通过泛型就可以控制 函数/类/接口 接收形参时的具体类型。所以总结就是支持动态类型、以及可以实现类型约束

参数化类型

可以考虑以下2 个需求如何实现:

  1. 我们需要打印或获取某变量参数类型;
  2. 我们已有一个接口定义,现在只是期望把所有属性变成可选;

需要怎么实现呢?需求 1 暴力做法是把能想到的参数类型都枚举列出、显然麻烦一些、且不够灵活,因为我们并不期望预置固定类型、反而希望它能灵活处理、在调用时做限制:

typescript 复制代码
// 暴力写法
function print(arg: string | number | boolean): string | number | boolean {
    console.log(typeof arg);
    return arg;
}

// 泛型解法
function print<T>(arg: T): T {
    console.log(typeof arg);
    return arg;
}

需求 2 暴力写法就是重新声明一份接口、加上可选修饰符?

typescript 复制代码
// 暴力写法
interface IUser {
  name: string;
  age: number;
  sex: string;
}
interface IUserPartial {
  name?: string;
  age?: number;
  sex?: string;
}
const user: IUser = { name: 'cat' }; // Error: Type '{ name: string; }' is missing the following properties from type 'IUser': age, sexts(2739)
const user: IUserPartial = { name: 'cat' }; // ok

// 泛型
type Partial<T> = {
    [P in keyof T]?: T[P];
};
const user: Partial<IUser> = { name: 'cat' }; // ok

再比如实现数组/元组项交换例子:

typescript 复制代码
function swap<T, U>(tuple: [T, U]): [U, T] {
    return [tuple[1], tuple[0]];
}

swap(['cat', 'dog']); // ['dog', 'cat']
swap([3, 'dog']); // ['dog', 3]

类型约束

另外、可以利用泛型约束更精准控制函数调用参数,更可预期、有助于提高代码质量和可维护性,比如我们实现对象merge 功能:

typescript 复制代码
function merge<T extends U, U>(target: T, source: U): T {
    for (let id in source) {
        target[id] = (<T>source)[id];
    }
    return target;
}

const obj = { a: 1, b: 2, c: 3, d: 4 };

merge(obj, { b: 10, d: 20 }); // { a: 1, b: 10, c: 3, d: 20 }
merge(obj, { b: 10, e: 20 }); // Error: Property 'e' is missing in type '{ a: number; b: number; c: number; d: number; }' but required in type '{ b: number; e: number; }'.ts(2345)

另外关于类型约束常见的场景就是对接 API 实现数据请求功能,我们通常期望对 API 格式、字段值、请求库返回结构等做好约定、提高程序稳定性、以及开发效率:

typescript 复制代码
// src/api/type.ts
// API 定义接口,可以是具体的 url,可选、可用于做请求 url拦截
interface IApi {
  '/app/list': any;
	'/app/detail': any;
	[key: string]: Record<string, unknown>;
}

// API 返回格式 接口
interface IResponseData<T> {
  code: 0 | 1 | 2 | 3;
  data: T;
  message: string;
}

// 具体数据格式接口
interface IUserData {
  name: string;
  age: number;
}

// 通用请求 类型定义
type TCommonRequest<T, U> = (url: Extract<keyof T, string>, params: Record<string, unknown>) => Promise<U | undefined>;
// 业务请求 类型定义
type TRequest<T> = TCommonRequest<IApi, IResponseData<T>>;
typescript 复制代码
// src/api/index.ts
// 定义 API 常量
const Api = {
  app: {
    list: '/app/list',
    detail: '/app/detail',
  },
};
typescript 复制代码
// src/components/User.tsx

// 业务组件实现 数据请求功能代码
const request: TRequest<IUserData> = async (url, params) => {
  const res = await fetch(url, params);
  return res.json();
};

async function fetchData() {
  const user = await request(Api.app.list, {});
	// outcome log
  // {
  //  code: 0,
  //  result: { name: 'cat', age: 3 },
  //  message: '请求成功'
  // }
  console.log(user);
}

fetchData();

联合类型

联合类型的常用场景之一是通过多个对象类型的联合,实现手动的互斥属性,即这一属性如果有字段1,那就没有字段2:

typescript 复制代码
interface IUser {
  info:
    | {
        vip: true;
        expires: string;
      }
    | {
        vip: false;
        promotion: string;
      };
}

declare let user:IUser;

if (user.info.vip) {
  console.log(user.info.expires); // ok
	console.log(user.info.promotion); // Error: Property 'promotion' does not exist on type '{ vip: true; expires: string; }'.ts(2339)
}

TypeScript 工程问题

项目配置

eslint 配置

推荐统一通过 eslint 进行项目配置(tslint 已废弃):

bash 复制代码
# 使用npm 安装
npm install --save-dev eslint
npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

# 使用yarn 安装
yarn add --dev eslint
yarn add  --dev @typescript-eslint/parser @typescript-eslint/eslint-plugin
  • @typescript-eslint/parser:ESLint 解析器,用于解析Typescript,从而检查和规范Typescript代码;
  • @typescript-eslint/eslint-plugin:ESLint 插件,包含了各类定义好的Typescript 代码检测规范。
json 复制代码
// .eslintrc.json 文件
{
  "parser": "@typescript-eslint/parser", // 定义ESLint的解析器
  "plugins": ["@typescript-eslint"], // 定义了该eslint文件所依赖的插件
  "extends": [ // 定义文件继承的子规范
    "plugin:@typescript-eslint/recommended"
  ],
	"env":{    //指定代码的运行环境
    "browser": true,
    "node": true,
	},
	"parserOptions": {        //指定ESLint可以解析JSX语法
    "ecmaVersion": 2019,
    "sourceType": "module",
    "ecmaFeatures": {
      "jsx": true,
		},
  },
	"rules": {
		"quotes": "off",
    "@typescript-eslint/quotes": ["error", "single"],
  },
}

// package.json 文件添加脚本
"scripts": {
  "eslint": "eslint src/**"
}

// 执行脚本
npm run eslint

prettier 配置

bash 复制代码
// 安装
npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier

配置.prettierrc.js 文件:

js 复制代码
module.exports =  {
  "printWidth": 120,
  "semi": false,
  "singleQuote": true,
  "trailingComma": "all",
  "bracketSpacing": false,
  "jsxBracketSameLine": true,
  "arrowParens": "avoid",
  "insertPragma": true,
  "tabWidth": 4,
  "useTabs": false  
};

.eslintrc.json文件中extends 下添加配置:

json 复制代码
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
  • prettier/@typescript-eslint:替代@typescript-eslint 中代码样式规范,改为遵循prettier中的样式规范;
  • plugin:prettier/recommended:给 ESlint 检测规则添加prettier 代码样式规范,代码格式问题会默认以error 的形式展示;

最后你还可以添加Huskylint-staged用来在代码提交前做好代码规范、风格检测。

类型导入

我们知道、TypeScript 类型只是在编译时添加类型检测,运行时并不存在、或者说构建JavaScript 代码时会擦除类型相关代码,以代码为例:

typescript 复制代码
type TBasicInfo = {
	name: string;
}
type TUser = TBasicInfo & { religion: string  };

const setUser = (user: TUser) => {
	const name = user?.name;
}

转换后的代码类似如下,类型代码被擦除、语法做了转换:

typescript 复制代码
const setUser = (user) => {
	var name;
	if (user && user.name) {
		name = user.name;
	}
}

再来看一个模块导入 case:

typescript 复制代码
// ./foo.ts
export function doThing(options: Options) {
  // ...
}

// ./bar.ts
import { doThing } from './foo.ts';

function doThingBetter(options: Options) {
  doThing(options);
}

上面代码转换后会去除 Options,因为可以明确它是一个类型;但当我们遇到如下模块导入场景、就会出现问题:无法判断Thing 是模块还是类型,如果 Thing 是类型、则编译出的代码无法正确运行、因为运行时并不支持类型代码

typescript 复制代码
import { Thing } from './module.ts';

export { Thing };

三方库兼容

declare module/namespace

在项目中我们很可能会使用大量第三方库、可能会遇到其中一些库不支持类型声明,或者团队历史工具库不支持 TypeScript,使用时 eslint 检测会有错误提示:

可以通过 declare modulenamespace的方式来提供其类型,declare module通常用于为没有提供类型定义的库进行类型的补全:

typescript 复制代码
// Nlib/index.d.ts
declare module 'Nlib' {
  export const getLength: () => number;
  export default getLength;
}

// or namespace
declare namespace Nlib {
  export function getLength: () => number;
}

模块和命名空间的作用基本一致、都可以包含代码和声明,主要的不同之处在于:

  1. 模块可以声明它的依赖、import 其他模块;
  2. 命名空间最明确的目的就是解决重名问题、本质是位于全局命名空间下的一个普通有名字的 JavaScript 对象;

除了为缺失类型的模块声明类型以外,我们还可以为非代码文件,如图片、CSS文件等声明类型,如 markdown 文件,其本质和 npm 导入规则一致,因此可以类似地使用 declare module 语法:

typescript 复制代码
// declare.d.ts
declare module '*.md' {
  const raw: string;
  export default raw;
}

// index.ts
import readme from './readme.md';

DefinitelyTyped

@types/ scope下的 npm 包类型均属于 DefinitelyTyped ,由TypeScript 维护,专用于为社区存在的无类型定义的JavaScript 库 添加类型支持,内部其实是数个 .d.ts 后缀的声明文件,常见的有 @types/react @types/lodash 等等。

相关推荐
golitter.2 分钟前
Ajax和axios简单用法
前端·ajax·okhttp
YUELEI1186 分钟前
TypeScript 封装 Axios 1.7.7
typescript·axios
雷特IT21 分钟前
Uncaught TypeError: 0 is not a function的解决方法
前端·javascript
长路 ㅤ   44 分钟前
vite学习教程02、vite+vue2配置环境变量
前端·vite·环境变量·跨环境配置
亚里士多没有德7751 小时前
强制删除了windows自带的edge浏览器,重装不了怎么办【已解决】
前端·edge
micro2010141 小时前
Microsoft Edge 离线安装包制作或获取方法和下载地址分享
前端·edge
.生产的驴1 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
awonw1 小时前
[前端][easyui]easyui select 默认值
前端·javascript·easyui
九圣残炎1 小时前
【Vue】vue-admin-template项目搭建
前端·vue.js·arcgis
柏箱2 小时前
使用JavaScript写一个网页端的四则运算器
前端·javascript·css