我正在寻找一种更好的方法来区分程序中不同类型的字符串 - 例如,绝对路径和相对路径。我希望能够让函数接受或返回某种类型的编译错误,如果我搞砸了。
例如,
RunAll "ColorMeYellow", "FormatDownload", "SheetCounter"
其中AbsolutePath和RelativePath实际上只是字符串。我尝试了类型别名,但实际上并没有创建新类型。接口 -
function makeAbsolute(path: RelativePath): AbsolutePath {
}
但由于这些接口是兼容的,编译器不会阻止我将它们混合起来。如果没有向接口添加属性以使它们不兼容(并且实际上将该属性添加到字符串或者围绕它转换)或使用包装类,我不知道如何做到这一点。还有其他想法吗?
答案 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`
我们可以将此行为用于"标记"字符串为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;
}
我们必须使用我们的&#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"
在你写一本关于它的书的每个级别上都是错的...... - 但它 工作得很好, 完成工作
由于他们的创建是这样控制的,AbsolutePath
和RelativePath
个实例是:
String
,允许调用字符串函数这类似于伪造的继承&#34; (因为TS编译器被告知有关继承,但在运行时不存在该继承)并且需要额外的数据验证。由于没有添加公共成员或方法,因此不应该导致意外的运行时行为,因为在编译和运行时期间都存在相同的假设功能。