TypeScript - 特定的字符串类型

Posted

技术标签:

【中文标题】TypeScript - 特定的字符串类型【英文标题】:TypeScript - specific string types 【发布时间】:2016-08-31 10:19:48 【问题描述】:

我正在寻找一种更好的方法来区分程序中不同类型的字符串——例如,绝对路径和相对路径。如果我搞砸了,我希望能够让函数采用或返回带有编译器错误的某种类型。

例如,

function makeAbsolute(path: RelativePath): AbsolutePath 

其中 AbsolutePath 和 RelativePath 真的只是字符串。我尝试了类型别名,但实际上并没有创建新类型。还有接口 -

interface AbsolutePath extends String  
interface RelativePath extends String  

但由于这些接口是兼容的,编译器不会阻止我将它们混合在一起。如果不向接口添加属性以使其不兼容(并且实际上将该属性添加到字符串或围绕它进行强制转换),或者使用包装类,我看不到如何做到这一点。还有其他想法吗?

【问题讨论】:

我不确定我是否关注你。您想要不同的类型,具体取决于字符串包含的内容?如果是这样,您可能想查看 lib.d.ts 中的document.createElement,我相信它使用字符串值包含的内容来确定函数重载。例如,当您提供"DIV" 作为参数时,它将返回htmlDivElement 不,不取决于字符串的内容。只有两种类型,实际上是任何字符串,但编译器将它们视为不兼容。 看看是否可以欺骗 TS 中的结构类型系统以某种方式实现这一点会很有趣。 @Alex There. @JohnWhite 哎呀,我的浏览器没有显示 :) 【参考方案1】:
abstract class RelativePath extends String 
    public static createFromString(url: string): RelativePath 
        // validate if 'url' is indeed a relative path
        // for example, if it does not begin with '/'
        // ...
        return url as any;
    

    private __relativePathFlag;


abstract class AbsolutePath extends String 
    public static createFromString(url: string): AbsolutePath 
        // validate if 'url' is indeed an absolute path
        // for example, if it begins with '/'
        // ...
        return url as any;
    

    private __absolutePathFlag;

var path1 = RelativePath.createFromString("relative/path");
var path2 = AbsolutePath.createFromString("/absolute/path");

// Compile error: type 'AbsolutePath' is not assignable to type 'RelativePath'
path1 = path2;

console.log(typeof path1); // "string"
console.log(typeof path2); // "string"
console.log(path1.toUpperCase()); // "RELATIVE/PATH"

这在你可以写一本关于它的书的各个层面上都是错误的...... - 但它确实工作得很好,而且它确实完成了工作.

由于它们的创建是这样控制的,AbsolutePathRelativePath 实例是:

被TS编译器认为相互不兼容(因为私有属性) 被 TS 编译器认为是(继承自)String,允许调用字符串函数 运行时真正的字符串,为假定继承的字符串函数提供运行时支持

这类似于带有附加数据验证的“假继承”(因为 TS 编译器被告知继承,但该继承在运行时不存在)。由于没有添加公共成员或方法,因此这绝不会导致意外的运行时行为,因为在编译和运行时都存在相同的假定功能。

【讨论】:

哈哈,老实说,我不认为它像你说的那样 hacky - 一个实际上并没有创建新对象的类肯定会引起一些人的注意。一个优点是它为执行运行时验证字符串实际上是预期格式提供了一个位置。但缺点是我会在很多地方调用那些“构造函数”助手,例如我在任何地方都与 Node 的“路径”模块交互。但是对于任何解决方案,我最终都会在这种情况下进行铸造或其他事情。 @RobLourens 是的,虽然我相信增加的冗长是一个合理的代价,而且以后肯定会有它的优势。 +1 为了努力,也许是唯一可行的解​​决方案类型,但我仍然很好奇是否有人可以定义真正的结构兼容类型,而编译器仍然无法确认它们的兼容性。虽然这听起来确实像一个错误...... +1 一件事让我很感兴趣。生成的 javascript 与仅使用字符串的原始 javascript 有何不同?我喜欢类型检查,以及在编译时而不是运行时捕获错误的能力,但也想知道潜在开销的缺点。 @Fede url 只是通过创建者函数。如果内部没有检查,唯一的开销就是函数调用本身。【参考方案2】:

有几种方法可以做到这一点。所有这些都涉及使用交叉点“标记”目标类型。

枚举标记

我们可以利用 TypeScript 中有一种名义类型 - the Enum type 的事实来区分其他结构上相同的类型:

枚举类型是 Number 基本类型的不同的子类型

这是什么意思?

接口和类在结构上进行比较

interface First 
interface Second 

var x: First;
var y: Second;
x = y; // Compiles because First and Second are structurally equivalent

枚举基于它们的“身份”而不同(例如,它们是主格类型)

const enum First 
const enum Second 

var x: First;
var y: Second;
x = y;  // Compilation error: Type 'Second' is not assignable to type 'First'.

我们可以通过以下两种方式之一利用Enum 的名义输入来“标记”或“标记”我们的结构类型:

使用枚举类型标记类型

由于 Typescript 支持交集类型和类型别名,我们可以用枚举“标记”任何类型并将其标记为新类型。然后我们可以毫无问题地将基类型的任何实例转换为“标记”类型:

const enum MyTag 
type SpecialString = string & MyTag;
var x = 'I am special' as SpecialString;
// The type of x is `string & MyTag`

我们可以使用这种行为将字符串“标记”为RelativeAbsolute 路径(如果我们想标记number,这将不起作用 - 请参阅第二个选项了解如何处理这些情况) :

declare module Path 
  export const enum Relative 
  export const enum Absolute 


type RelativePath = string & Path.Relative;
type AbsolutePath = string & Path.Absolute;
type Path = RelativePath | AbsolutePath

然后我们可以简单地将字符串的任何实例“标记”为任何类型的Path

var path = 'thing/here' as Path;
var absolutePath = '/really/rooted' as AbsolutePath;

但是,我们投射时没有检查到位,因此可以:

var assertedAbsolute = 'really/relative' as AbsolutePath;
// compiles without issue, fails at runtime somewhere else

为了缓解这个问题,我们可以使用基于控制流的类型检查来确保我们仅在测试通过时进行强制转换(在运行时):

function isRelative(path: String): path is RelativePath 
  return path.substr(0, 1) !== '/';


function isAbsolute(path: String): path is AbsolutePath 
  return !isRelative(path);

然后使用它们来确保我们正在处理正确的类型没有任何运行时错误

var path = 'thing/here' as Path;
if (isRelative(path)) 
  // path's type is now string & Relative
  withRelativePath(path);
 else 
  // path's type is now string & Absolute
  withAbsolutePath(path);

接口/类的通用结构“品牌”

不幸的是,我们不能标记 number 子类型,如 WeightVelocity,因为 Typescript 足够聪明,可以将 number & SomeEnum 减少到仅 number。我们可以使用泛型和字段来“标记”类或接口并获得类似的名义类型行为。这类似于@JohnWhite 用他的私人名字建议的,但只要泛型是enum,就不会发生名称冲突:

/**
 * Nominal typing for any TypeScript interface or class.
 *
 * If T is an enum type, any type which includes this interface
 * will only match other types that are tagged with the same
 * enum type.
 */
interface Nominal<T>  'nominal structural brand': T 

// Alternatively, you can use an abstract class
// If you make the type argument `T extends string`
// instead of `T /* must be enum */`
// then you can avoid the need for enums, at the cost of
// collisions if you choose the same string as someone else
abstract class As<T extends string> 
  private _nominativeBrand: T;


declare module Path 
  export const enum Relative 
  export const enum Absolute 

type BasePath<T> = Nominal<T> & string
type RelativePath = BasePath<Path.Relative>
type AbsolutePath = BasePath<Path.Absolute>
type Path = RelativePath | AbsolutePath

// Mark that this string is a Path of some kind
// (The alternative is to use
// var path = 'thing/here' as Path
// which is all this function does).
function toPath(path: string): Path 
  return path as Path;

我们必须使用我们的“构造函数”从基本类型创建我们的“品牌”类型的实例:

var path = toPath('thing/here');
// or a type cast will also do the trick
var path = 'thing/here' as Path

同样,我们可以使用基于控制流的类型和函数来获得额外的编译时安全性:

if (isRelative(path)) 
  withRelativePath(path);
 else 
  withAbsolutePath(path);

另外,这也适用于number 子类型:

declare module Dates 
  export const enum Year 
  export const enum Month 
  export const enum Day 


type DatePart<T> = Nominal<T> & number
type Year = DatePart<Dates.Year>
type Month = DatePart<Dates.Month>
type Day = DatePart<Dates.Day>

var ageInYears = 30 as Year;
var ageInDays: Day;
ageInDays = ageInYears;
// Compilation error:
// Type 'Nominal<Month> & number' is not assignable to type 'Nominal<Year> & number'.

改编自https://github.com/Microsoft/TypeScript/issues/185#issuecomment-125988288

【讨论】:

这是个好主意,但我也可以用其他接口上不存在的任意属性替换枚举和品牌,对吧? 我想这样做的价值在于它是可扩展的,因为我不会想出唯一的属性名称或其他东西。而当品牌名称始终相同时,人们看到它就会知道它的含义。 (需要明确的是,我将其与 interface Relative extends String _fakeProperty1: any; 进行对比。我更喜欢这个。) @RobLourens - 更新了更多解释以及使用交叉点类型的替代方法。 @RobLourens 我喜欢你正在做的事情,但随后投射对我来说会引发错误。 不确定它在哪个 TS 版本中工作,但至少在最新 (3.9.2) 中它 工作: const enum MyTag const enum MyOtherTag type SpecialString = 字符串 & MyTag;类型 OtherSpecialString = 字符串 & MyOtherTag; const x: SpecialString = '我很特别' as SpecialString;常量 y: OtherSpecialString = x; // 两者都没有错误类型是“从不”

以上是关于TypeScript - 特定的字符串类型的主要内容,如果未能解决你的问题,请参考以下文章

Typescript 对象:如何将键限制为特定字符串?

如何键入 Typescript 数组以仅接受一组特定的值?

Typescript自定义类型定义在导入类上中断

TypeScript

TypeScript

TypeScript