TypeScript特定的字符串类型

Rob*_*ens 13 types typescript

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

例如,

function makeAbsolute(path: RelativePath): AbsolutePath {
}
Run Code Online (Sandbox Code Playgroud)

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

interface AbsolutePath extends String { }
interface RelativePath extends String { }
Run Code Online (Sandbox Code Playgroud)

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

Sea*_*ira 14

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

枚举标记

我们可以利用的事实,那就是在一个打字稿名义类型- Enum类型来区分,否则同样的结构类型:

枚举类型是Number原始类型的不同子类型

这是什么意思?

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

interface First {}
interface Second {}

var x: First;
var y: Second;
x = y; // Compiles because First and Second are structurally equivalent
Run Code Online (Sandbox Code Playgroud)

枚举根据其"身份"而不同(例如,它们是主格键入的)

const enum First {}
const enum Second {}

var x: First;
var y: Second;
x = y;  // Compilation error: Type 'Second' is not assignable to type 'First'.
Run Code Online (Sandbox Code Playgroud)

我们可以利用Enum标称类型来以两种方式之一"标记"或"标记"我们的结构类型:

使用枚举类型标记类型

由于Typescript支持交集类型和类型别名,我们可以使用枚举"标记"任何类型并将其标记为新类型.然后我们可以将基类型的任何实例转换为"标记"类型而不会出现问题:

const enum MyTag {}
type SpecialString = string & MyTag;
var x = 'I am special' as SpecialString;
// The type of x is `string & MyTag`
Run Code Online (Sandbox Code Playgroud)

我们可以使用这种行为将字符串"标记"为存在RelativeAbsolute路径(如果我们想要标记a,这将不起作用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
Run Code Online (Sandbox Code Playgroud)

然后我们可以Path通过强制转换将任何字符串实例"标记"为任何类型:

var path = 'thing/here' as Path;
var absolutePath = '/really/rooted' as AbsolutePath;
Run Code Online (Sandbox Code Playgroud)

但是,当我们施放时没有检查到位,所以可以:

var assertedAbsolute = 'really/relative' as AbsolutePath;
// compiles without issue, fails at runtime somewhere else
Run Code Online (Sandbox Code Playgroud)

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

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

function isAbsolute(path: String): path is AbsolutePath {
  return !isRelative(path);
}
Run Code Online (Sandbox Code Playgroud)

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

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);
}
Run Code Online (Sandbox Code Playgroud)

接口/类的通用结构"品牌"

不幸的是,我们无法标记number子类型,Weight或者Velocity因为Typescript足够聪明,可以简化number & SomeEnumnumber.我们可以使用泛型和字段来"标记"类或接口,并获得类似的名义类型行为.这类似于@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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

if (isRelative(path)) {
  withRelativePath(path);
} else {
  withAbsolutePath(path);
}
Run Code Online (Sandbox Code Playgroud)

并且,作为额外的奖励,这也适用于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'.
Run Code Online (Sandbox Code Playgroud)

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

  • 我想这样做的价值在于它是可扩展的,因为我不会想出独特的属性名称或其他东西.当品牌名称始终相同时,人们会在看到它时知道它的含义.(很明显,我将它与`interface Relative extends String {_fakeProperty1:any;}`进行对比.我比这更喜欢这个.) (3认同)
  • 不确定这在哪个 TS 版本中有效,但至少在最新版本(3.9.2)中它*不*工作: const enum MyTag {} const enum MyOtherTag {} type SpecialString = string &amp; MyTag; 类型 OtherSpecialString = string &amp; MyOtherTag; const x: SpecialString = '我很特殊' as SpecialString; const y: 其他特殊字符串 = x; // 没有错误 两者的类型都是“从不” (2认同)

Joh*_*isz 7

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;
}
Run Code Online (Sandbox Code Playgroud)
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"
Run Code Online (Sandbox Code Playgroud)

在你写一本关于它的书的每一个层面上都是错的...... - 但它确实很好用,它确实完成了工作.

自创建以来被控制为这样的,AbsolutePathRelativePath实例是:

  • 相信TS编译器彼此不兼容(因为私有财产)
  • 相信是StringTS编译器(继承自),允许调用字符串函数
  • 实际上是运行时的真正字符串,为所谓的继承字符串函数提供运行时支持

这类似于"伪造的继承"(因为TS编译器被告知继承,但在运行时不存在继承)以及额外的数据验证.由于没有添加任何公共成员或方法,因此不应该导致意外的运行时行为,因为在编译和运行时期间都存在相同的假设功能.