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 链接
