网站搜索

typescript - 运行时的“映射类型”?


对于这里极其冗长的“最小复现”表示歉意,但我对 TypeScript 相当陌生,不确定还有什么其他方式提出我的问题,所以我想我就从这里开始吧:

// just a generic "Failure" type from which all other "Failures" are derived:
class Failure { constructor (public message: string) {} }
type FailureResult<R> = R extends Failure ? R : never;

// anything which is not a Failure is a Success.
type SuccessResult<R> = R extends Failure ? never : R;

// the purpose of the above is to have a bunch of functions living in an object...
type MultipleChecks<C, R> = {
  [K in keyof R]: (context: C) => R[K]
}

// and once all the functions have been called, to divide the result(s) into
// a type whose members are all some type of Failure (or null)...
type MappedFailure<R> = {
  [K in keyof R]: FailureResult<R[K]>|null
};
// ... or a type whose members are all successes:
type MappedSuccess<R> = {
  [K in keyof R]: SuccessResult<R[K]>
};

// probably wasn't strictly necessary for the minimal repro, but for the record I'm also
// taking those combined failures and turning them into a single failure:
class MultipleFailures<R> extends Failure {
  readonly failures: MappedFailure<R>;

  constructor (failures: MappedFailure<R>) {
    super('multiple failures');
    this.failures = failures;
  }
}

type MappedResult<R> = MappedSuccess<R>|MultipleFailures<R>;

// Everything up until this part works as I expect. this is the part
// that I can't figure out how to implement correctly:
function validateMultiple<C, R> (context: C, checks: MultipleChecks<C, R>) : MappedResult<R> {
  const results : MappedSuccess<R> = {} as MappedSuccess<R>
  const failures : MappedFailure<R> = {} as MappedFailure<R>
  let checkFailed = false;

  for (const key of Object.keys(checks) as [keyof MultipleChecks<C, R>]) {
    const fn = checks[key];
    const result = fn(context);
    if (result instanceof Failure) {
      checkFailed = true;
      // @ts-expect-error Type 'R[keyof R] & Failure' is not assignable
      // to type 'FailureResult<R[keyof R]>'.
      failures[key] = result;
    } else {
      // @ts-expect-error Type 'R[string]' is not assignable to type
      //'SuccessResult<R[keyof R]>'.
      results[key] = result;
      failures[key] = null;
    }
  }
  if (checkFailed) return new MultipleFailures(failures);
  else return results;
}

包括一些示例实现,以防使用情况能更清楚地说明我在问什么:

class NumberTooSmall extends Failure {
  constructor () { super('number too small.'); }
}

class NumberTooBig extends Failure {
  constructor () { super('number too big.'); }
}

type NumberResult = { a: number, b: number } | NumberTooSmall | NumberTooBig;

function validateNumbers ({a, b}: { a: number , b: number }) : NumberResult {
  if (a < 1 || b < 1) return new NumberTooSmall();
  if (a > 100 || b > 100) return new NumberTooBig();
  return { a, b };
};

class NameTooShort extends Failure {
  constructor () { super('name too short.'); }
}

class NameTooLong extends Failure {
  constructor () { super('name too long.'); }
}

type NameResult = string | NameTooShort | NameTooLong;

function validateName ({ name }: { name: string }) : NameResult {
  if (name.length < 2) return new NameTooShort();
  if (name.length > 5) return new NameTooLong();
  return name;
};

function addByName (context: { a: number, b: number, name: string }) : string {
  const validation = validateMultiple(context, {
    numbers: validateNumbers,
    name: validateName
  });

  if (validation instanceof MultipleFailures) {
    let message = 'Failed to add:'
    for (const f of Object.values(validation.failures)) {
      if (f !== null) message += `\n${f.message}`
    }
    return message;
  } else {
    const { name, numbers } = validation;
    const { a, b } = numbers;
    return `${name} added ${a} + ${b} and got ${a + b}.`;
  }
}

既然已经把这些解决了,我会尝试口头解释我的想法。使用顶部的两种类型FailureResult和SuccessResult,我将一个函数的返回类型分为失败和成功,因此:

type FooContext = { fooInput: string }
type FooResult = Foo|FooErrorA|FooErrorB
function getFoo (context: FooContext) : FooResult;
type FooSuccess = SuccessResult<FooResult>; // i.e. Foo
type FooFailure = FailureResult<FooResult>; // i.e. FooErrorA|FooErrorB

那部分工作正常。关键是我希望能够将这样的函数收集到一个对象中,依次调用每个函数,然后让结果被“映射”到一个对象中,该对象要么全是成功,要么全是 Failure|null。从类型上来看,这似乎也可以正常工作,使用 MappedSuccess<R> 和 MappedFailure<R> 类型:

type MultiContext = FooContext & BarContext;
type MultiChecks = { foo: FooResult, bar: BarResult };

// either every field has a non-error value, or the results are condensed into
// a failure object where each field is the failure or null.
type AllSuccess = { foo: FooSuccess, bar: BarSuccess };
type SomeFailures = { foo: FooFailure|null, bar: BarFailure|null }
type MultiResult =  AllSuccess|{ failures: SomeFailures }
function validateMultiple(context: MultiContext, MultiChecks) : MultiResult;

问题(如在 validateMultiple 的示例实现中所示)是,我似乎无法让 TypeScript 相信在我遍历对象时生成的中间值是安全的。我很确定它们至少在非正式情况下是安全的。我当然不会对需要做一些类型断言感到惊讶,但我甚至无法弄清楚那些断言会是什么。

答案#1

在 ValidateMultiple 的实现内部,result 的类型是 R[keyof R],并且可能会或可能不会缩小为 R[keyof R] & Failure。但是编译器并不认为这两种类型可以赋值给 failures[key] 或 results[k] 的类型。相关的 FailureResult<XXX> 和 SuccessResult<XXX> 类型是条件类型,当它们依赖于尚未指定的类型参数(如 R)时,它们几乎被视为完全不透明。编译器无法知道可能实际可赋值给 results[key] 的具体值,因此它会报错:

if (result instanceof Failure) {
  checkFailed = true;
  failures[key] = result; // error!
  // not assignable to type 'FailureResult<R[keyof R]> | null'.
} else {
  results[key] = result; // error!
  // not assignable to type 'SuccessResult<R[keyof R]>'.
  failures[key] = null;
}

如果你关心的只是抑制错误(而不是进行重大重构以试图说服编译器你在做安全的事情),那么对每个错误进行类型断言是合理的。

你可以使用错误信息来了解应该使用哪些断言。在上面的情况中,它表示 result 不能分配给 FailureResult<R[keyof R]> | null,所以大概这就是我们希望在这种情况下将 result 分配给的类型。因此我们可以断言它是 FailureResult<R[keyof R]>(因为我们知道它不是 null)。在下面的情况中,我们有 result 不能分配给 SuccessResult<R[keyof T]>,所以同样,这似乎是一个在这种情况下适合断言的类型:

if (result instanceof Failure) {
  checkFailed = true;
  failures[key] = result as FailureResult<R[keyof R]>; // okay
} else {
  results[key] = result as SuccessResult<R[keyof R]> ; // okay
  failures[key] = null;
}

现在没有错误了,如所愿。

代码的 Playground 链接