热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

编写高效TS的一些建议

本文阿宝哥将分享编写高效TS代码的5个建议,希望这些建议对大家编写TS代码能有一些帮助。一、尽量减少重复代码对于刚接触TypeScript的小伙伴来说,

本文阿宝哥将分享编写高效 TS 代码的 5 个建议,希望这些建议对大家编写 TS 代码能有一些帮助。

一、尽量减少重复代码

对于刚接触 TypeScript 的小伙伴来说,在定义接口时,可能一不小心会出现以下类似的重复代码。比如:

interface Person {firstName: string;lastName: string;
}interface PersonWithBirthDate {firstName: string;lastName: string;birth: Date;
}

很明显,相对于 Person 接口来说,PersonWithBirthDate 接口只是多了一个 birth 属性,其他的属性跟 Person 接口是一样的。那么如何避免出现例子中的重复代码呢?要解决这个问题,可以利用 extends 关键字:

interface Person { firstName: string; lastName: string;
}interface PersonWithBirthDate extends Person { birth: Date;
}

当然除了使用 extends 关键字之外,也可以使用交叉运算符(&):

type PersonWithBirthDate = Person & { birth: Date };

另外,有时候你可能还会发现自己想要定义一个类型来匹配一个初始配置对象的「形状」,比如:

const INIT_OPTIONS = {width: 640,height: 480,color: "#00FF00",label: "VGA",
};interface Options {width: number;height: number;color: string;label: string;
}

其实,对于 Options 接口来说,你也可以使用 typeof 操作符来快速获取配置对象的「形状」:

type Options = typeof INIT_OPTIONS;

而在使用可辨识联合(代数数据类型或标签联合类型)的过程中,也可能出现重复代码。比如:

interface SaveAction { type: 'save';// ...
}interface LoadAction {type: 'load';// ...
}type Action = SaveAction | LoadAction;
type ActionType = 'save' | 'load'; // Repeated types!

为了避免重复定义 'save''load',我们可以使用成员访问语法,来提取对象中属性的类型:

type ActionType = Action['type']; // 类型是 "save" | "load"

然而在实际的开发过程中,重复的类型并不总是那么容易被发现。有时它们会被语法所掩盖。比如有多个函数拥有相同的类型签名:

function get(url: string, opts: Options): Promise { /* ... */ } 
function post(url: string, opts: Options): Promise { /* ... */ }

对于上面的 get 和 post 方法,为了避免重复的代码,你可以提取统一的类型签名:

type HTTPFunction = (url: string, opts: Options) => Promise; const get: HTTPFunction = (url, opts) => { /* ... */ };
const post: HTTPFunction = (url, opts) => { /* ... */ };

二、使用更精确的类型替代字符串类型

假设你正在构建一个音乐集,并希望为专辑定义一个类型。这时你可以使用 interface 关键字来定义一个 Album 类型:

interface Album {artist: string; // 艺术家title: string; // 专辑标题releaseDate: string; // 发行日期:YYYY-MM-DDrecordingType: string; // 录制类型:"live" 或 "studio"
}

对于 Album 类型,你希望 releaseDate 属性值的格式为 YYYY-MM-DD,而 recordingType 属性值的范围为 livestudio。但因为接口中 releaseDaterecordingType 属性的类型都是字符串,所以在使用 Album 接口时,可能会出现以下问题:

const dangerous: Album = {artist: "Michael Jackson",title: "Dangerous",releaseDate: "November 31, 1991", // 与预期格式不匹配recordingType: "Studio", // 与预期格式不匹配
};

虽然 releaseDaterecordingType 的值与预期的格式不匹配,但此时 TypeScript 编译器并不能发现该问题。为了解决这个问题,你应该为 releaseDaterecordingType 属性定义更精确的类型,比如这样:

interface Album {artist: string; // 艺术家title: string; // 专辑标题releaseDate: Date; // 发行日期:YYYY-MM-DDrecordingType: "studio" | "live"; // 录制类型:"live" 或 "studio"
}

重新定义 Album 接口之后,对于前面的赋值语句,TypeScript 编译器就会提示以下异常信息:

const dangerous: Album = {artist: "Michael Jackson",title: "Dangerous",// 不能将类型“string”分配给类型“Date”。ts(2322)releaseDate: "November 31, 1991", // Error// 不能将类型“"Studio"”分配给类型“"studio" | "live"”。ts(2322)recordingType: "Studio", // Error
};

为了解决上面的问题,你需要为 releaseDaterecordingType 属性设置正确的类型,比如这样:

const dangerous: Album = {artist: "Michael Jackson",title: "Dangerous",releaseDate: new Date("1991-11-31"),recordingType: "studio",
};

另一个错误使用字符串类型的场景是设置函数的参数类型。假设你需要写一个函数,用于从一个对象数组中抽取某个属性的值并保存到数组中,在 Underscore 库中,这个操作被称为 “pluck”。要实现该功能,你可能最先想到以下代码:

function pluck(record: any[], key: string): any[] {return record.map((r) => r[key]);
}

对于以上的 pluck 函数并不是很好,因为它使用了 any 类型,特别是作为返回值的类型。那么如何优化 pluck 函数呢?首先,可以通过引入一个泛型参数来改善类型签名:

function pluck(record: T[], key: string): any[] {// Element implicitly has an 'any' type because expression of type 'string' can't be used to // index type 'unknown'.// No index signature with a parameter of type 'string' was found on type 'unknown'.(7053)return record.map((r) => r[key]); // Error
}

通过以上的异常信息,可知字符串类型的 key 不能被作为 unknown 类型的索引类型。要从对象上获取某个属性的值,你需要保证参数 key 是对象中的属性。

说到这里相信有一些小伙伴已经想到了 keyof 操作符,它是 TypeScript 2.1 版本引入的,可用于获取某种类型的所有键,其返回类型是联合类型。接着使用 keyof 操作符来更新一下 pluck 函数:

function pluck(record: T[], key: keyof T) {return record.map((r) => r[key]);
}

对于更新后的 pluck 函数,你的 IDE 将会为你自动推断出该函数的返回类型:

function pluck(record: T[], key: keyof T): T[keyof T][]

对于更新后的 pluck 函数,你可以使用前面定义的 Album 类型来测试一下:

const albums: Album[] = [{artist: "Michael Jackson",title: "Dangerous",releaseDate: new Date("1991-11-31"),recordingType: "studio",
}];// let releaseDateArr: (string | Date)[]
let releaseDateArr = pluck(albums, 'releaseDate');

示例中的 releaseDateArr 变量,它的类型被推断为 (string | Date)[],很明显这并不是你所期望的,它的正确类型应该是 Date[]。那么应该如何解决该问题呢?这时你需要引入第二个泛型参数 K,然后使用 extends 来进行约束:

function pluck(record: T[], key: K): T[K][] {return record.map((r) => r[key]);
}// let releaseDateArr: Date[]
let releaseDateArr = pluck(albums, 'releaseDate');

三、定义的类型总是表示有效的状态

假设你正在构建一个允许用户指定页码,然后加载并显示该页面对应内容的 Web 应用程序。首先,你可能会先定义 State 对象:

interface State {pageContent: string;isLoading: boolean;errorMsg?: string;
}

接着你会定义一个 renderPage 函数,用来渲染页面:

function renderPage(state: State) {if (state.errorMsg) {return `呜呜呜,加载页面出现异常了...${state.errorMsg}`;} else if (state.isLoading) {return `页面加载中~~~`;}return `

${state.pageContent}
`;
}// 输出结果:页面加载中~~~
console.log(renderPage({isLoading: true, pageContent: ""}));
// 输出结果:
大家好,我是阿宝哥

console.log(renderPage({isLoading: false, pageContent: "大家好,我是阿宝哥"}));

创建好 renderPage 函数,你可以继续定义一个 changePage 函数,用于根据页码获取对应的页面数据:

async function changePage(state: State, newPage: string) {state.isLoading = true;try {const response = await fetch(getUrlForPage(newPage));if (!response.ok) {throw new Error(`Unable to load ${newPage}: ${response.statusText}`);}const text = await response.text();state.isLoading = false;state.pageContent = text;} catch (e) {state.errorMsg = "" + e;}
}

对于以上的 changePage 函数,它存在以下问题:

  • 在 catch 语句中,未把 state.isLoading 的状态设置为 false;

  • 未及时清理 state.errorMsg 的值,因此如果之前的请求失败,那么你将继续看到错误消息,而不是加载消息。

出现上述问题的原因是,前面定义的 State 类型允许同时设置 isLoadingerrorMsg 的值,尽管这是一种无效的状态。针对这个问题,你可以考虑引入可辨识联合类型来定义不同的页面请求状态:

interface RequestPending {state: "pending";
}interface RequestError {state: "error";errorMsg: string;
}interface RequestSuccess {state: "ok";pageContent: string;
}type RequestState = RequestPending | RequestError | RequestSuccess;interface State {currentPage: string;requests: { [page: string]: RequestState };
}

在以上代码中,通过使用可辨识联合类型分别定义了 3 种不同的请求状态,这样就可以很容易的区分出不同的请求状态,从而让业务逻辑处理更加清晰。接下来,需要基于更新后的 State 类型,来分别更新一下前面创建的 renderPagechangePage 函数:

更新后的 renderPage 函数

function renderPage(state: State) {const { currentPage } = state;const requestState = state.requests[currentPage];switch (requestState.state) {case "pending":return `页面加载中~~~`;case "error":return `呜呜呜,加载第${currentPage}页出现异常了...${requestState.errorMsg}`;case "ok":`

第${currentPage}页的内容:${requestState.pageContent}
`;}
}

更新后的 changePage 函数

async function changePage(state: State, newPage: string) {state.requests[newPage] = { state: "pending" };state.currentPage = newPage;try {const response = await fetch(getUrlForPage(newPage));if (!response.ok) {throw new Error(`无法正常加载页面 ${newPage}: ${response.statusText}`);}const pageContent = await response.text();state.requests[newPage] = { state: "ok", pageContent };} catch (e) {state.requests[newPage] = { state: "error", errorMsg: "" + e };}
}

changePage 函数中,会根据不同的情形设置不同的请求状态,而不同的请求状态会包含不同的信息。这样 renderPage 函数就可以根据统一的 state 属性值来进行相应的处理。因此,通过使用可辨识联合类型,让请求的每种状态都是有效的状态,不会出现无效状态的问题。

四、选择条件类型而不是重载声明

假设你要使用 TS 实现一个 double 函数,该函数支持 stringnumber 类型。这时,你可能已经想到了使用联合类型和函数重载:

function double(x: number | string): number | string;
function double(x: any) {return x + x;
}

虽然这个 double 函数的声明是正确的,但它有一点不精确:

// const num: string | number
const num = double(10); 
// const str: string | number
const str = double('ts');

对于 double 函数,你期望传入的参数类型是 number 类型,其返回值的类型也是 number 类型。当你传入的参数类型是 string 类型,其返回的类型也是 string 类型。而上面的 double 函数却是返回了 string | number 类型。为了实现上述的要求,你可能想到了引入泛型变量和泛型约束:

function double(x: T): T;
function double(x: any) {return x + x;
}

在上面的 double 函数中,引入了泛型变量 T,同时使用 extends 约束了其类型范围。

// const num: 10
const num = double(10);
// const str: "ts"
const str = double('ts');
console.log(str);

不幸的是,我们对精确度的追求超过了预期。现在的类型有点太精确了。当传递一个字符串类型时,double 声明将返回一个字符串类型,这是正确的。但是当传递一个字符串字面量类型时,返回的类型是相同的字符串字面量类型。这是错误的,因为 ts 经过 double 函数处理后,返回的是 tsts,而不是 ts

另一种方案是提供多种类型声明。虽然 TypeScript 只允许你编写一个具体的实现,但它允许你编写任意数量的类型声明。你可以使用函数重载来改善 double 的类型:

function double(x: number): number;
function double(x: string): string;
function double(x: any) {return x + x;
}// const num: number
const num = double(10); 
// const str: string
const str = double("ts"); 

很明显此时 num 和 str 变量的类型都是正确的,但不幸的是,double 函数还有一个小问题。因为 double 函数的声明只支持 stringnumber 类型的值,而不支持 string | number 联合类型,比如:

function doubleFn(x: number | string) {// Argument of type 'string | number' is not assignable to // parameter of type 'number'.// Argument of type 'string | number' is not assignable to // parameter of type 'string'.return double(x); // Error
}

为什么会提示以上的错误呢?因为当 TypeScript 编译器处理函数重载时,它会查找重载列表,直到找一个匹配的签名。对于 number | string 联合类型,很明显是匹配失败的。

然而对于上述的问题,虽然可以通过新增 string | number 的重载签名来解决,但最好的方案是使用条件类型。在类型空间中,条件类型就像 if 语句一样:

function double(x: T
): T extends string ? string : number;
function double(x: any) {return x + x;
}

这与前面引入泛型版本的 double 函数声明类似,只是它引入更复杂的返回类型。条件类型使用起来很简单,与 Javascript 中的三目运算符(?:)一样的规则。T extends string ? string : number 的意思是,如果 T 类型是 string 类型的子集,则 double 函数的返回值类型为 string 类型,否则为 number 类型。

在引入条件类型之后,前面的所有例子就可以正常工作了:

// const num: number
const num = double(10); 
// const str: string
const str = double("ts"); // function f(x: string | number): string | number
function f(x: number | string) {return double(x);
}

五、一次性创建对象

在 Javascript 中可以很容易地创建一个表示二维坐标点的对象:

const pt = {}; 
pt.x = 3; 
pt.y = 4;

然而对于同样的代码,在 TypeScript 中会提示以下错误信息:

const pt = {};
// Property 'x' does not exist on type '{}'
pt.x = 3; // Error
// Property 'y' does not exist on type '{}'
pt.y = 4; // Error

这是因为第一行中 pt 变量的类型是根据它的值 {} 推断出来的,你只能对已知的属性赋值。针对这个问题,你可能会想到一种解决方案,即新声明一个 Point 类型,然后把它作为 pt 变量的类型:

interface Point {x: number;y: number;
}// Type '{}' is missing the following properties from type 'Point': x, y(2739)
const pt: Point = {}; // Error
pt.x = 3;
pt.y = 4;

那么如何解决上述问题呢?其中一种最简单的解决方案是一次性创建对象:

const pt = { x: 3,y: 4, 
}; // OK

如果你想一步一步地创建对象,你可以使用类型断言(as)来消除类型检查:

const pt = {} as Point; 
pt.x = 3;
pt.y = 4; // OK

但是更好的方法是一次性创建对象并显式声明变量的类型:

const pt: Point = { x: 3,y: 4, 
};

而当你需要从较小的对象来构建一个较大的对象时,你可能会这样处理,比如:

const pt = { x: 3, y: 4 };
const id = { name: "Pythagoras" };
const namedPoint = {};
Object.assign(namedPoint, pt, id);// Property 'id' does not exist on type '{}'.(2339)
namedPoint.name; // Error

为了解决上述问题,你可以使用对象展开运算符 ... 来一次性构建大的对象:

const namedPoint = {...pt, ...id}; 
namedPoint.name; // OK, type is string

此外,你还可以使用对象展开运算符,以一种类型安全的方式逐个字段地构建对象。关键是在每次更新时使用一个新变量,这样每个变量都会得到一个新类型:

const pt0 = {};
const pt1 = {...pt0, x: 3};
const pt: Point = {...pt1, y: 4}; // OK

虽然这是构建这样一个简单对象的一种迂回方式,但对于向对象添加属性并允许 TypeScript 推断新类型来说,这可能是一种有用的技术。要以类型安全的方式有条件地添加属性,可以使用带 null{} 的对象展开运算符,它不会添加任何属性:

declare var hasMiddle: boolean;
const firstLast = {first: 'Harry', last: 'Truman'};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};

如果在编辑器中鼠标移到 president,你将看到 TypeScript 推断出的类型:

const president: {middle?: string;first: string;last: string;
}

最终通过设置 hasMiddle 变量的值,你就可以控制 president 对象中 middle 属性的值:

declare var hasMiddle: boolean;
var hasMiddle = true;
const firstLast = {first: 'Harry', last: 'Truman'};
const president = {...firstLast, ...(hasMiddle ? {middle: 'S'} : {})};let mid = president.middle
console.log(mid); // S

六、参考资源

  • effective-typescript-specific-ways-improve



推荐阅读
  • Java太阳系小游戏分析和源码详解
    本文介绍了一个基于Java的太阳系小游戏的分析和源码详解。通过对面向对象的知识的学习和实践,作者实现了太阳系各行星绕太阳转的效果。文章详细介绍了游戏的设计思路和源码结构,包括工具类、常量、图片加载、面板等。通过这个小游戏的制作,读者可以巩固和应用所学的知识,如类的继承、方法的重载与重写、多态和封装等。 ... [详细]
  • vue使用
    关键词: ... [详细]
  • 电话号码的字母组合解题思路和代码示例
    本文介绍了力扣题目《电话号码的字母组合》的解题思路和代码示例。通过使用哈希表和递归求解的方法,可以将给定的电话号码转换为对应的字母组合。详细的解题思路和代码示例可以帮助读者更好地理解和实现该题目。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • PHP图片截取方法及应用实例
    本文介绍了使用PHP动态切割JPEG图片的方法,并提供了应用实例,包括截取视频图、提取文章内容中的图片地址、裁切图片等问题。详细介绍了相关的PHP函数和参数的使用,以及图片切割的具体步骤。同时,还提供了一些注意事项和优化建议。通过本文的学习,读者可以掌握PHP图片截取的技巧,实现自己的需求。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • Spring特性实现接口多类的动态调用详解
    本文详细介绍了如何使用Spring特性实现接口多类的动态调用。通过对Spring IoC容器的基础类BeanFactory和ApplicationContext的介绍,以及getBeansOfType方法的应用,解决了在实际工作中遇到的接口及多个实现类的问题。同时,文章还提到了SPI使用的不便之处,并介绍了借助ApplicationContext实现需求的方法。阅读本文,你将了解到Spring特性的实现原理和实际应用方式。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • web.py开发web 第八章 Formalchemy 服务端验证方法
    本文介绍了在web.py开发中使用Formalchemy进行服务端表单数据验证的方法。以User表单为例,详细说明了对各字段的验证要求,包括必填、长度限制、唯一性等。同时介绍了如何自定义验证方法来实现验证唯一性和两个密码是否相等的功能。该文提供了相关代码示例。 ... [详细]
author-avatar
U曹宸prideX_J
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有