首页 > 解决方案 > 可分配给未标记默认值的标记类

问题描述

我有一个错误类,它可能有也可能没有一些标签来识别出了什么问题(一个字符串)。如果潜在值在签名中可见,我认为这将很有用,例如:

function f(): 'success' | MyError<'invalid'|'timeout'>

到目前为止,这似乎很容易。但是,问题在于“或可能不”部分,具有将 any 分配MyError<T>给 default的能力MyError,以及构造函数的参数的必要性。从逻辑上讲,如果签名说“没有给出额外的信息”,那么可能会给出源自较低级别的任何错误,这可能有额外的信息,而该信息从未被访问过。

我的第一次尝试是简单地string用作默认值,然后分配没问题:

class MyError<T extends string = string> {
  info: T;
  constructor(init: T) { this.info = init; }
}
let a: MyError = new MyError<'something'>('something');

这种方法有两个问题:

第一个可以通过在顶部修补一个额外的类型来解决:

type InfoError<T extends MyError> = string extends T['info'] ? Omit<T, 'info'> : T;

let b = new MyError<'something'>('something');
let c: InfoError<MyError> = b;

然而,这带来的问题多于解决的问题。一切都需要显式输入,所以签名是正确的,当只是返回时new MyError('completely useless'),它仍然需要一个参数。

有没有更容易被我忽略的东西?


最后一次蛮力尝试(我认为有效,强调“思考”。在这种情况下,“有效”意味着类型安全,尽管有any-casts,只要它__CompoundError本身从未被导出),要折叠的片段:

class __CompoundError<T extends string> {
  tag: T;
  constructor(tag?: T) {
    if (tag === undefined) this.tag = '' as any;
    else this.tag = tag;
  }
}

Object.defineProperty(__CompoundError, 'name', {
  value: 'CompoundError',
  configurable: true,
});

type CompoundError<T extends string> = string extends T ? Omit<__CompoundError<T>, 'tag'> : __CompoundError<T>;

const CompoundError: {
    new <T extends string>(tag: T): CompoundError<T>;
    new (): CompoundError<string>
  } = __CompoundError as any;

// ################# tests #################

let generic = new CompoundError(); // don't need any argument for generic version
let a = new CompoundError('str'); // generic type inferred
let b: CompoundError<'str' | 'other'>;

// generic.tag // error, generic doesn't have property tag
a.tag; // ok

generic = a; // ok, can assign to generic version
b = a; // ok, can assign to more broad version

console.log(a.constructor.name); // 'CompoundError'

declare function f(): CompoundError<'str'>; // can use as a type
function g() { return new CompoundError('str'); } // can just return value and infer function type

与我可能最终会使用的类似版本:

class CompoundError {}

class CompoundErrorTagged<T extends string> extends CompoundError {
  constructor(public tag: T) { super(); }
}

// ################# tests #################

let generic = new CompoundError(); // don't need any argument for generic version
let a = new CompoundErrorTagged('str'); // generic type inferred
let b: CompoundErrorTagged<'str' | 'other'>;

// generic.tag // error, generic doesn't have property tag
a.tag; // ok

generic = a; // ok, can assign to generic version
b = a; // ok, can assign to more broad version

console.log(a.constructor.name); // 'CompoundErrorTagged'

declare function f(): CompoundErrorTagged<'str'>; // can use as a type
function g() { return new CompoundErrorTagged('str'); } // can just return value and infer function type

标签: typescript

解决方案


这里的方法是将完全没有标签的错误视为完全不同的类型。

class TaggedError<T extends string> {
  constructor(public info: T) { }
}

class UntaggedError {
  constructor(/* Other untagged error arguments? */) {}
}

type MyError<T extends string> = TaggedError<T> | UntaggedError;

现在MyError<T>包括错误实际上可能未被标记的可能性。infoTypescript 将在这里保护您,在您确定错误是标记还是未标记之前,它不会让您访问该属性。

function f(): 'success' | MyError<'invalid' | 'timeout'>;

const result = f();

if (result === 'success') {
  // ... success!
}

// ERROR: Property info does not exist on type UntaggedError
console.log(result.info);

if ('info' in result) {
  console.log(`TaggedError<${result.info}>`);
} else {
  console.log('UntaggedError... I dunno what happened!');
}

如果你不喜欢'info' in result类型缩小的形式,你总是可以写一个 typeguard。

function isTaggedError<T>(e: MyError<T>): e is TaggedError<T> {
  return 'info' in e;
}

if(isTaggedError(result)) { // type inference just works here
  console.log(`TaggedError<${result.info}>`);
}


推荐阅读