有几种方法可以做到这一点。所有这些都涉及使用交叉点“标记”目标类型。
枚举标记
我们可以利用 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`
我们可以使用这种行为将字符串“标记”为Relative 或Absolute 路径(如果我们想标记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 子类型,如 Weight 或 Velocity,因为 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