vlambda博客
学习文章列表

10 个关于 TypeScript 的小技巧


英文 | https://www.sangle7.com/


1、 TypeScript 和 DOM

当你开始使用 TypeScript 时,你会发现在浏览器环境中使用它,你需要非常了解它。假设我想在页面搜索框里找到一个 元素:
 
   
   
 
const textEl = document.querySelector('inpot');console.log(textEl.value);// 🛑 Property 'value' does not exist on type 'Element'.
Oops…… 抛出了一个错误,因为我把 ‘input’ 打成了 ‘inpot’
它怎么知道的?答案在于 lib.dom.d.ts 文件,该文件是 TypeScript 库的一部分,并且基本上描述了浏览器中发生的所有事情(对象,函数,事件)。 
该定义的一部分是在 querySelector 方法的输入中使用的接口,并将特定的字符串文字(例如’div’, ‘table’或’input’)映射到相应的 HTML 元素类型:
 
   
   
 
interface HTMLElementTagNameMap { a: HTMLAnchorElement; abbr: HTMLElement; address: HTMLElement; applet: HTMLAppletElement; area: HTMLAreaElement; article: HTMLElement; /* ... */ input: HTMLInputElement; /* ... */}
这不是一个完美的解决方案,因为它仅适用于基本元素选择器,但总比没有好,对吧?
这种’智能’TypeScript 行为的另一个示例是在处理浏览器事件时:
 
   
   
 
textEl.addEventListener('click', (e) => { console.log(e.clientX);});
上面的示例中的.clientX 在任何给定事件上都不可用-仅在 MouseEvent 上可用。然后 TypeScript 根据作为 addEventListener 方法中第一个参数的“click”文字确定事件的类型。

2、期望泛型

因此,如果您使用其他任何东西而不是元素选择器:
 
   
   
 
document.querySelector('input.action')
那么 HTMLELementTagNameMap 将不再有用,TypeScript 只会返回一个相当基本的 Element 类型。
与 querySelector 一样,函数通常可以返回各种不同的结构,而 TypeScript 不可能确定将是哪种结构。在那种情况下,您可以非常期待,该函数也是通用的,并且可以使用方便的通用语法提供该返回类型:
 
   
   
 
textEl = document.querySelector < HTMLInputElement > 'input.action';console.log(textEl.value);// 👍 'value' is available because we've instructed TS// about the type the 'querySelector' function works with.

3、“我们真的找到了吗?”

该 document.querySelector(…)方法实际上并不总是返回一个对象,是吗?与选择器匹配的元素可能不在页面上-函数将返回 null 而不是对象。因此,默认情况下,访问.value 属性可能不会保存所有内容。
默认情况下,类型检查器认为 null 和 undefined 可分配给任何类型。您可以通过在 tsconfig.json 中添加严格的 null 检查来使其更加安全并限制这种行为:
 
   
   
 
{ "compilerOptions": { "strictNullChecks": true }}
使用该设置后,如果您尝试访问可能为 null 的对象上的属性,TypeScript 将会报错,并且你将不得不确保该对象的存在,例如 通过用 if(textEl){...} 条件包装该部分。
除了 querySelector 之外,另一个流行的例子是 Array.find 方法,其结果可能是不确定的。
您并非总能找到想要的东西:-)
4、“TS,我告诉你,在这里!”
正如我们已经确定的那样,通过严格的 null 检查,TypeScript 将更加怀疑我们的价值观。另一方面,有时您仅从外部就知道将设置该值。在这种特殊情况下,您可以使用“后缀表达式运算符”:
 
   
   
 
const textEl = document.querySelector('input');console.log(textEl!.value);// 👍 with "!" we assure TypeScript// that 'textEl' is not null/undefined

5、当迁移到 TS…

通常,当您具有要迁移到 TypeScript 的旧版代码库时,更大的麻烦之一就是使 id 遵守您的 TSLint 规则。您可以做的是通过添加以下内容来编辑所有这些文件
 
   
   
 
// tslint:disable
在每个文件的第一行中,这样 TSLint 不会真正检查它们。然后,仅当开发人员处理旧文件时,他才会删除此注释并仅修复该文件中的所有掉毛错误。这样一来,我们就不会进行革命,而只会进行进化-代码库会逐渐但安全地得到改善。
至于将实际类型添加到旧的 JavaScript 代码中,实际上通常可以不这样做。只有在您有一些令人讨厌的代码(例如, 为同一变量分配不同类型的值,您可能会遇到问题。如果重构不是一个小问题,您可以使用这个方法解决问题:
 
   
   
 
let mything = 2;mything = 'hi';// 🛑 Type '"hi"' is not assignable to type 'number'mything = 'hi' as any;// 👍 if you say "any", TypeScript says ¯\_(ツ)_/¯
但是真的,真的,真的将其用作最后的手段。我们不喜欢TypeScript中的 any。

6、更多限制

有时TypeScript无法推断类型。最常见的情况是一个函数参数:
 
   
   
 
function fn(param) { console.log(param);}
在内部,它需要在此处为param分配某种类型,因此它可以分配任何类型。由于我们希望将any限制为绝对最小值,因此通常建议使用另一个tsconfig.json设置来限制该行为:
 
   
   
 
{ "compilerOptions": { "noImplicitAny": true }}
不幸的是,我们不能在函数返回类型上使用这种安全带(需要明确输入)。因此,如果改为使用函数fn(param):string {我会忘记该类型(函数fn(param){),TypeScript将不会关注我返回的内容,即使我从该函数返回了任何内容。更准确地说:它将根据您退回或未退回的商品推断出退货价值。
幸运的是,TSLint可以为您提供帮助。使用typedef规则,您可以使返回类型成为必需:
 
   
   
 
{ "rules": { "typedef": [ true, "call-signature" ] }}
这看起来是个好主意!

7、类型保护

当值具有多种类型时,必须在算法中将其考虑在内,以区分一种类型与另一种类型。关于TypeScript的事情是它了解这种逻辑。
 
   
   
 
type BookId = number | string;function returnFormatterId(id: BookId) { return id.toUpperCase(); // 🛑 'toUpperCase' does not exist on type 'number'.}function returnFormatterId(id: BookId) { if (typeof id === 'string') { // we've made sure it's a string: return id.toUpperCase(); // so it's 👍 } // 👍 TS also understands that it // has to be a number here: return id.toFixed(2)}

8、再谈泛型

假设我们具有这种相当通用的结构:
 
   
   
 
interface Bookmark { id: string;}class BookmarksService { items: Bookmark[] = [];}
您想在不同的应用程序中使用它,例如 用于存储书籍或电影。
在这样的应用程序中,您可以执行以下操作:
 
   
   
 
interface Movie { id: string; name: string;}class SearchPageComponent { movie: Movie; constructor(private bs: BookmarksService) {} getFirstMovie() { // 🛑 types are not assignable this.movie = this.bs.items[0]; // 👍 so you have to manually assert type: this.movie = this.bs.items[0] as Movie; } getSecondMovie() { this.movie = this.bs.items[1] as Movie; }}
在该类中可能需要多次这种类型声明。
我们可以做的是将 BookmarksService 类定义为通用类:
 
   
   
 
class BookmarksService<T> { items: T[] = [];}
好吧,不过现在它太通用了……我们要确保此类使用的类型能够满足Bookmark接口(即具有id:string属性)。这是改进的声明:
 
   
   
 
class BookmarksService<T extends Bookmark> { items: T[] = [];}
现在,在我们的SearchPageComponent中,我们只需指定一次类型:
 
   
   
 
class SearchPageComponent { movie: Movie; constructor(private bs: BookmarksService<Movie>) {} getFirstMovie() { this.movie = this.bs.items[0]; // 👍 } getSecondMovie() { this.movie = this.bs.items[1]; // 👍 }}
对于该通用类,还有一项可能是有用的改进-如果您以这种通用身份在其他地方使用它,而又不想编写BookmarksService  的话。
您可以在泛型的定义中提供默认类型:
 
   
   
 
class BookmarksService<T extends Bookmark = Bookmark> { items: T[] = [];}const bs = new BookmarksService();// I don't have to provide the type for the generic now// - in that case 'bs' will be of that default type 'Bookmark'

9、路由参数

 
   
   
 
export interface DashboardRouteParams { countryId: string; deviceId: string;}
const routes: Routes = [{ path: 'country/:countryId/device/:deviceId/dashboard'}]

10、根据 API 响应创建 Interface

如果您收到来自API的大量嵌套响应,那么手动键入相应的接口确实很麻烦。这是应该由机器处理的任务!🤖
有两种选择:
  • http://www.json2ts.com
  • http://www.jsontots.com
是其中的一些,但是坦率地说,它们的服务器通常不可用。由于URL的记忆力很强,我通常只是从它们开始:-)为了获得最佳结果和一些其他选项,请使用
  • https://app.quicktype.io/
它还提供了一个方便的Visual Studio Code插件~