标称键入TypeScript或如何保护界面不受外来标识符的影响


最近,在研究家庭项目操作不正确的原因时,我再次注意到由于疲劳而经常重复发生的错误。 错误的本质在于,在一个代码块中有多个标识符,当我调用函数时,我传递了另一种类型的对象的标识符。 在本文中,我将讨论如何使用TypeScript解决此问题。


一点理论


TypeScript基于结构化类型,非常适合JavaScript的鸭子思想。 关于这一点的文章已经足够多了。 我不会重复它们,我只会概述与名词性输入的主要区别,后者在其他语言中更常见。 让我们来看一个小例子。


class Car { id: number; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: number; move (x: number, y: number) { //   } } let car: Car = new Boat(); //  TypeScript   let boat: Boat = new Car(); //        

为什么TypeScript会有这种行为? 这只是结构化类型的体现。 与监视类型名称的名词性不同,结构化类型根据类型的内容决定类型的兼容性。 Car类包含Boat类的所有属性和方法,因此Car可以用作Boat。 相反,这是不正确的,因为Boat不具有numberOfWheels属性。


键入标识符


首先,我们将设置标识符的类型


 type CarId: number; type BoatId: number; 

并使用这些类型重写类。


 class Car { id: CarId; numberOfWheels: number; move (x: number, y: number) { //   } } class Boat { id: BoatId; move (x: number, y: number) { //   } } 

您会注意到情况并没有太大变化,因为我们仍然无法控制从何处获取标识符,您将是对的。 但是这个例子已经提供了一些优点。


  1. 在程序开发过程中,标识符的类型可能会突然改变。 因此,例如,可以用一个字符串VIN号代替该项目独有的某个车号。 如果不指定标识符的类型,则必须在所有出现数字的地方都用字符串替换数字。 对于类型任务,只需要在确定类型本身的一个地方进行更改。


  2. 调用函数时,我们会从代码编辑器获得提示,即应该使用什么类型标识符。 假设我们声明了以下函数:


     function getCarById(id: CarId): Car { // ... } function getBoatById(id: BoatId): Boat { // ... } 

    然后,编辑器会提示我们不仅必须传送数字,还必须传送CarId或BoatId。



模拟最严格的打字


TypeScript中没有名义上的类型,但是我们可以模拟其行为,使任何类型都是唯一的。 为此,将唯一的属性添加到类型。 该技巧在英文文章的“品牌”一词中进行了介绍,其外观如下:


 type BoatId = number & { _type: 'BoatId'}; type CarId = number & { _type: 'CarId'}; 

指出我们的类型必须既是数字又是具有唯一值的具有属性的对象,我们在理解结构类型时使我们的类型不兼容。 让我们看看它是如何工作的。


 let carId: CarId; let boatId: BoatId; let car: Car; let boat: Boat; car = getCarById(carId); // OK car = getCarById(boatId); // ERROR boat = getBoatById(boatId); // OK boat = getBoatById(carId); // ERROR carId = 1; // ERROR boatId = 2; // ERROR car = getCarById(3); // ERROR boat = getBoatById(4); // ERROR 

除了最后四行外,其他所有内容看起来都不错。 要创建标识符,您需要一个辅助函数:


 function makeCarIdFromVin(id: number): CarId { return vin as any; } 

这种方法的缺点是该函数将保留在运行时中。


使强类型输入不那么严格


在最后一个示例中,我不得不使用其他功能来创建标识符。 您可以使用Flavor接口定义来摆脱它:


 interface Flavoring<FlavorT> { _type?: FlavorT; } export type Flavor<T, FlavorT> = T & Flavoring<FlavorT>; 

现在,您可以按以下方式设置标识符的类型:


 type CarId = Flavor<number, “CarId”> type BoatId = Flavor<number, “BoatId”> 

由于_type属性是可选的,因此可以使用隐式转换:


 let boatId: BoatId = 5; // OK let carId: CarId = 3; // OK 

而且我们仍然不能混淆标识符:


 let carId: CarId = boatId; // ERROR 

选择哪个选项


两种选择都有权存在。 品牌化具有保护变量免于直接赋值的优势。 如果该变量以某种格式(例如,绝对文件路径,日期或IP地址)存储字符串,这将很有用。 在这种情况下,处理类型转换的帮助程序功能也可以检查和处理输入数据。 在其他情况下,使用Flavor更方便。


资料来源


  1. 起点stackoverflow.com
  2. 免费解释文章

Source: https://habr.com/ru/post/zh-CN446768/


All Articles