TypeScript特定的字符串类型

时间:2016-05-05 14:54:56

标签: types typescript

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

例如,

RunAll "ColorMeYellow", "FormatDownload", "SheetCounter"

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

function makeAbsolute(path: RelativePath): AbsolutePath {
}

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

2 个答案:

答案 0 :(得分:14)

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

枚举标记

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

  

枚举类型是数字基元类型

不同子类型

这是什么意思?

在结构上比较接口和类

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);
}

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

很遗憾,我们无法标记numberWeightVelocity个子类型,因为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;
}

我们必须使用我们的&#34;构造函数&#34;创建我们&#34;品牌&#34;的实例基类型的类型:

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

答案 1 :(得分: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;
}
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,允许调用字符串函数
  • 在运行时实际上是真正的字符串,为所谓的继承字符串函数提供运行时支持

这类似于伪造的继承&#34; (因为TS编译器被告知有关继承,但在运行时不存在该继承)并且需要额外的数据验证。由于没有添加公共成员或方法,因此不应该导致意外的运行时行为,因为在编译和运行时期间都存在相同的假设功能。