Неправильный вывод 'any' в ситуации, связанной с общей композицией функций и Rust-подобным типом 'Result'

  • В нашей кодовой базе есть тип, похожий по духу на Rust Result или Haskell's Either.
  • Я определил функцию, которая состоит из двух 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 сможет вывести их обоих.

Другие вопросы по тегам