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是一门强类型语言、类型规则是严格执行的,它的优势是:
- 有助于更早发现不稳定性问题:如对某变量操作使用不当、在 string 上使用number 相关操作、在未来的某一刻很可能暴雷;
- 更有利于多人或跨组件协作:通过智能提示、代码检测工具能更有效地进行代码推断、提高代码质量和协作效率,如我们可以更快捷的使用第三方库、清晰的知道 API 需要的数据格式、类型,也无需手工 check 是否遗漏关键信息。
编程范式
支持函数式和面向对象的编程方式写代码,在JavaScript 的基础上更强调面向对象特性,如继承、类、可见性范围,命名空间等,有更完整的代码组织能力。
- 面向对象编程(Object Oriented Programming) :TypeScript 提供了丰富的面向对象编程特性,包括类、接口、泛型等。可以极大的提升代码组织和复用性、在常见的代码迁移/重构场景优势明显;
- 函数式编程(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),语法不一样;
type TBasicInfo = {
name: string;
}
type TUser = TBasicInfo & { religion: string };
interface IUser extends TBasicInfo {
religion: string;
}
差异点
- type 可以声明基本类型别名,联合类型,元组等类型;
- interface 能够声明合并,支持实现(implements);
type StringOrNumber = string | number;
interface User {
name: string;
}
interface User {
age: string;
}
// Last output =
interface User {
name: string;
age: number;
}
善用枚举
枚举(Enum)类型用于取值被限定在一定范围内的场景,比如一周只能有七天,颜色限定为红绿蓝等。
枚举可以让代码拥有更好的类型提示,同时可以把常量真正地约束在一个命名空间下,最常见的场景就是项目中常量声明。
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 时类型推导与检查不会生效;
let obj: any = null;
// 以下代码均无异常提示
obj.getCoordinate();
obj.x.y;
unknown:表示类型未知、但未来会确定类型。
let obj: unknown = null;
obj.getCoordinate(); // 报错:obj类型为 unknown
(obj as { getCoordinate: () => string }).foo(); // ok
never:代表无类型,不带类型信息,常见的是 throwerror 函数。
function throwError(): never {
throw new Error()
}
有时候也可以用它来做流程分支运行时安全/错误检查、兜底:
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:此类成员仅支持在类与子类中被访问。
// 在构造函数中对参数设置访问性修饰符,等价于 单独声明类属性然后在构造函数中进行一一赋值
class User {
constructor(public name: string, private age: number) { }
}
new User('Didi', 22);
static 私有构造函数
当类的构造函数标记为private 后,就只允许在类内部访问了,主要作用场景是定义工具库、工具库内都是静态方法,并不期望被实例化:
class Utils {
public static tactics = ['money', 'number', 'ratio'];
private constructor(){}
public static transferNum() {
}
}
Utils.transferNum();
泛型
泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
泛型的本质是为了参数化类型、即不创建新类型,通过泛型就可以控制 函数/类/接口 接收形参时的具体类型。所以总结就是支持动态类型、以及可以实现类型约束。
参数化类型
可以考虑以下2 个需求如何实现:
- 我们需要打印或获取某变量参数类型;
- 我们已有一个接口定义,现在只是期望把所有属性变成可选;
需要怎么实现呢?需求 1 暴力做法是把能想到的参数类型都枚举列出、显然麻烦一些、且不够灵活,因为我们并不期望预置固定类型、反而希望它能灵活处理、在调用时做限制:
// 暴力写法
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 暴力写法就是重新声明一份接口、加上可选修饰符?
:
// 暴力写法
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
再比如实现数组/元组项交换例子:
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 功能:
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 格式、字段值、请求库返回结构等做好约定、提高程序稳定性、以及开发效率:
// 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>>;
// src/api/index.ts
// 定义 API 常量
const Api = {
app: {
list: '/app/list',
detail: '/app/detail',
},
};
// 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:
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 已废弃):
# 使用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 代码检测规范。
// .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 配置
// 安装
npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier
配置.prettierrc.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
下添加配置:
"prettier/@typescript-eslint",
"plugin:prettier/recommended",
prettier/@typescript-eslint:
替代@typescript-eslint 中代码样式规范,改为遵循prettier中的样式规范;plugin:prettier/recommended:
给 ESlint 检测规则添加prettier 代码样式规范,代码格式问题会默认以error 的形式展示;
最后你还可以添加Husky
和lint-staged
用来在代码提交前做好代码规范、风格检测。
类型导入
我们知道、TypeScript 类型只是在编译时添加类型检测,运行时并不存在、或者说构建JavaScript 代码时会擦除类型相关代码,以代码为例:
type TBasicInfo = {
name: string;
}
type TUser = TBasicInfo & { religion: string };
const setUser = (user: TUser) => {
const name = user?.name;
}
转换后的代码类似如下,类型代码被擦除、语法做了转换:
const setUser = (user) => {
var name;
if (user && user.name) {
name = user.name;
}
}
再来看一个模块导入 case:
// ./foo.ts
export function doThing(options: Options) {
// ...
}
// ./bar.ts
import { doThing } from './foo.ts';
function doThingBetter(options: Options) {
doThing(options);
}
上面代码转换后会去除 Options,因为可以明确它是一个类型;但当我们遇到如下模块导入场景、就会出现问题:无法判断Thing 是模块还是类型,如果 Thing 是类型、则编译出的代码无法正确运行、因为运行时并不支持类型代码
。
import { Thing } from './module.ts';
export { Thing };
三方库兼容
declare module/namespace
在项目中我们很可能会使用大量第三方库、可能会遇到其中一些库不支持类型声明,或者团队历史工具库不支持 TypeScript,使用时 eslint 检测会有错误提示:
可以通过 declare module
或 namespace
的方式来提供其类型,declare module
通常用于为没有提供类型定义的库进行类型的补全:
// Nlib/index.d.ts
declare module 'Nlib' {
export const getLength: () => number;
export default getLength;
}
// or namespace
declare namespace Nlib {
export function getLength: () => number;
}
模块和命名空间的作用基本一致、都可以包含代码和声明,主要的不同之处在于:
- 模块可以声明它的依赖、import 其他模块;
- 命名空间最明确的目的就是解决重名问题、本质是位于全局命名空间下的一个普通有名字的 JavaScript 对象;
除了为缺失类型的模块声明类型以外,我们还可以为非代码文件,如图片、CSS文件等声明类型,如 markdown 文件,其本质和 npm 导入规则一致,因此可以类似地使用 declare module 语法:
// declare.d.ts
declare module '*.md' {
const raw: string;
export default raw;
}
// index.ts
import readme from './readme.md';
TypeScript 3.8 版本开始,支持了仅仅导入/导出声明语法来解决这个问题:
import type { SomeThing } from "./some-module.js";
export type { SomeThing };
DefinitelyTyped
@types/
scope下的 npm 包类型均属于 DefinitelyTyped ,由TypeScript 维护,专用于为社区存在的无类型定义的JavaScript 库添加类型支持,内部其实是数个 .d.ts
后缀的声明文件,常见的有 @types/react
@types/lodash
等等。
转载自:https://juejin.cn/post/7320433112197972020