Неправильный вывод 'any' в ситуации, связанной с общей композицией функций и Rust-подобным типом 'Result'
- В нашей кодовой базе есть тип, похожий по духу на Rust
Result
или Haskell'sEither
. - Я определил функцию, которая состоит из двух
ParseFunc
функция.
Моя проблема: когда я звоню
compose
, в некоторых случаях TypeScript определяет первый аргумент типа как
any
. Это пугает, потому что это происходит незаметно и приведет к тому, что последующие ошибки типа будут пропущены.
Пример было немного сложно свести к минимуму. Вот как далеко я зашел. (Ссылка на игровую площадку).
// A 'Result' type (similar in spirit to Rust's 'Result' or Haskell's 'Either')
export type Result<T, E> = ResultOk<T> | ResultErr<E>;
export const Result = {
ok<T, E = never>(value: T): Result<T, E> { return new ResultOk(value); },
err<E, T = never>(err: E): Result<T, E> { return new ResultErr(err); },
};
export class ResultOk<T> {
public readonly ok: true = true;
constructor(public readonly value: T) {}
}
export class ResultErr<E> {
public readonly ok: false = false;
constructor(public readonly err: E) {}
}
// A parsing library
type ParseFunc<OutT, InT> = (val: InT) => Result<OutT, string>;
declare const parseString: ParseFunc<string, unknown>;
declare const parseObject: <F extends {[s: string]: ParseFunc<any, any>}> (fields: F) => ParseFunc<unknown, unknown>;
declare function compose<OutT, MidT, InT>(
f1: ParseFunc<MidT, InT>,
f2: ParseFunc<OutT, MidT>,
): ParseFunc<OutT, InT>;
// PROBLEM: The first type argument to 'compose' is inferred as 'any' instead of 'string'.
// The 'any' causes the type checker to miss later errors.
const schema1 = parseObject({
f: compose(parseString, s => {
if (Math.random() < 0.5) return Result.err('blah');
return Result.ok('blah');
}),
});
// Problem goes away if I call 'new ResultOk/Err' instead of 'Result.ok/err'
// BUT that messes up many other uses of Result in my codebase.
const schema2 = parseObject({
f: compose(parseString, s => {
if (Math.random() < 0.5) return new ResultErr('blah');
return new ResultOk('blah');
}),
});
// Problem goes away if the arrow function is not declared inline.
const parseFunc2 = (s: string) => {
if (Math.random() < 0.5) return new ResultErr('blah');
return new ResultOk('blah');
}
const schema3 = parseObject({
f: compose(parseString, parseFunc2),
});
// Problem goes away if I don't wrap in 'z.object'.
// Maybe the issue has to do with the fact that 'z.object' has 'any' in its declaration?
// - https://github.com/colinhacks/zod/blob/fdd708493e4422dba6453908af55ef0d58767c03/src/types.ts#L1728-L1739
const fieldSchema = compose(parseString, s => {
if (Math.random() < 0.5) return new ResultErr('blah');
return new ResultOk('blah');
});
1 ответ
Проблема с типом функции. Значение переданного словаря всегда выводится как
ParseFunc<any,any>
.
declare const parseObject: <F extends {[s: string]: ParseFunc<any, any>}> (fields: F) => ParseFunc<unknown, unknown>;
Я имею в виду, что это вообще не предполагается.
Чтобы вывести это, вы должны изменить определение типа .
Рассмотрим этот пример:
declare const parseObject: <Out, In> (fields: Record<string, ParseFunc<Out, In>>) => ParseFunc<Out, In>;
Рабочее решение:
// A 'Result' type (similar in spirit to Rust's 'Result' or Haskell's 'Either')
export type Result<T, E> = ResultOk<T> | ResultErr<E>;
export const Result = {
ok<T, E = never>(value: T): Result<T, E> { return new ResultOk(value); },
err<E, T = never>(err: E): Result<T, E> { return new ResultErr(err); },
};
export class ResultOk<T> {
public readonly ok: true = true;
constructor(public readonly value: T) { }
}
export class ResultErr<E> {
public readonly ok: false = false;
constructor(public readonly err: E) { }
}
// A parsing library
type ParseFunc<OutT, InT> = (val: InT) => Result<OutT, string>;
declare const parseString: ParseFunc<string, unknown>;
declare const parseObject: <Out, In> (fields: Record<string, ParseFunc<Out, In>>) => ParseFunc<Out, In>;
declare function compose<OutT, MidT, InT>(
f1: ParseFunc<MidT, InT>,
f2: ParseFunc<OutT, MidT>,
): ParseFunc<OutT, InT>;
// ParseFunc<string, unknown>
const schema1 = parseObject({
f: compose(parseString, s => {
if (Math.random() < 0.5) return Result.err('blah');
return Result.ok('blah');
}),
});
// Problem goes away if I call 'new ResultOk/Err' instead of 'Result.ok/err'
// BUT that messes up many other uses of Result in my codebase.
const schema2 = parseObject({
f: compose(parseString, s => {
if (Math.random() < 0.5) return new ResultErr('blah');
return new ResultOk('blah');
}),
});
// Problem goes away if the arrow function is not declared inline.
const parseFunc2 = (s: string) => {
if (Math.random() < 0.5) return new ResultErr('blah');
return new ResultOk('blah');
}
const schema3 = parseObject({
f: compose(parseString, parseFunc2),
});
Давайте посмотрим на const, который вы использовали в своем первом нерабочем примере.
export type Result<T, E> = ResultOk<T> | ResultErr<E>;
export const Result = {
ok<T, E = never>(value: T): Result<T, E> { return new ResultOk(value); },
err<E, T = never>(err: E): Result<T, E> { return new ResultErr(err); },
};
// Result<number, never>
const ok = Result.ok(42)
// Result<never, string>
const error = Result.err('some error')
В соответствии с объявлением вы либо выводите тип, либо
error
тип, тогда как каждый
Result
следует знать о них обоих. См. Rust Result , каждое значение включает возможный тип значения и возможный тип ошибки. Теперь переключитесь на пример, который вы указали в своем вопросе, и наведите курсор на
Result.err('blah')
, ты получишь
Result<any, string>
. Это означает, что TS смог определить тип ошибки, но не смог
ok
тип. Как только вы измените объявление типа
parseObject
, TS сможет вывести их обоих.