我正在try 将Scott Wlaschin's "Railway Oriented Programming"个概念移植到打印本上.

我正在try 使either函数的类型正确.我认为我的下面的代码应该可以工作,但是我在either函数中的返回语句上收到了编译器错误.

type Success<T> = {
    success: true,
    result: T
};

type Failure<E> = {
    success: false,
    cause: E
}

export function either<T, E, U, F, I extends Success<T> | Failure<E>>(
    onSuccess: (i: T) => U,
    onFailure: (i: E) => F,
    input: I
  ): I extends Success<T> ? U : I extends Failure<E> ? F : never {
    if (input.success === true) {
      return onSuccess(input.result); // compiler error: Type 'U' is not assignable to type 'I extends Success<T> ? U : I extends Failure<E> ? F : never'
    } else {
      return onFailure(input.cause); // compiler error: Type 'F' is not assignable to type 'I extends Success<T> ? U : I extends Failure<E> ? F : never'
    }
  }

我不确定这个错误的原因,因为条件类型声明可以返回U和F,而我的代码导致输入参数类型的正确类型缩小.我可以通过使用与函数的返回类型(例如return onSuccess(input.result) as I extends Success<T> ? ....)匹配的Type断言来消除它们,但我的问题是:

  1. 这是消除编译器错误的唯一方法吗?
  2. 如果是这样,为什么类型脚本不能确定返回值满足指定的返回类型?

推荐答案

目前,TypeScrip的control flow anlaysis不影响generic个类型参数.如果具有泛型类型T的值t,则switch/caseif/else?/:类型保护t可以将t的表观类型从T缩小到其他值(例如,T & string),但它不能对类型参数T做任何事情.

这是有充分理由的:你不能就这么重新-constrain T.比方说T extends A | B,然后勾选isA(t),将t缩小到A(或T & A).这not个意味着T extends A个.T可能是A,也可能是完整的union type A | B.我们已经为T建立了新的lower bound,而不是新的upper bound.如果TypeScrip有一种方式来表达下限约束,就像microsoft/TypeScript#14520中所要求的那样,那么可能会写成T super A extends A | B.但它没有,所以我们有点被困住了,直到它得到实施.

你可能会认为T extends A | B的意图是T应该是exactly one of 103 or 104,而这些可能性的并集应该从一开始就被排除在考虑之外.那会很好,但这不是extends的意思.我们需要一种新的限制,就像在microsoft/TypeScript#27808中所要求的那样.我们就叫它oneof吧.然后,您可以编写T oneof A | B,而判断isA(t)确实会将T重新约束为T oneof A.但同样,这目前是不可能的,所以我们必须等待或解决它.

到目前为止,最简单的解决方法是使用type assertions来声明isA(t)确实意味着TA.


但您会问"这是消除编译器错误的唯一方法吗?"这个问题的答案是"不,但你可能无论如何都应该这么做".

现在,如果您想要使用泛型并拥有不带类型断言的类依赖类型,则需要进行重构,以便它不使用控制流分析,而是执行TypeScrip可以验证的为数不多的泛型操作之一.这主要涉及将具有泛型键类型的indexing转换为某个对象类型,或将mapped types转换为该对象类型.microsoft/TypeScript#47109中概述了一般方法.

不幸的是,对于您来说,重构是相当糟糕和引人注目的.您不能索引具有boolean个值的对象,因此我们需要改为某种字符串/数字/枚举类型:

enum Bool { FALSE, TRUE }
const _true = Bool.TRUE;
const _false = Bool.FALSE;

然后,您需要重写类型以始终涉及使用这些伪布尔键索引到对象上的显式映射类型.也许是这样的:

interface BoolMap<T, F> {
    [_true]: T,
    [_false]: F
}
type Either<T, F, B extends Bool> = { [P in B]:
    { success: P } & BoolMap<{ result: T }, { cause: F }>[P]
}[B]

function either<T, F, TO, FO, B extends Bool>(
    onSuccess: (i: T) => TO,
    onFailure: (i: F) => FO,
    input: Either<T, F, B>
) {
    const func: { [P in Bool]:
        (i: Either<T, F, P>) => BoolMap<TO, FO>[P]
    } = {
        [_true]: v => onSuccess(v.result),
        [_false]: v => onFailure(v.cause)
    };
    return func[input.success](input);
}

真恶心.但至少它编译时没有错误,也没有类型断言.让我们对其进行测试,以确保其仍按预期运行:

const succ = (x: string) => x.length;
const fail = (x: number) => x.toFixed(2);

const r1 = either(succ, fail, { success: _true, result: "abc" });
// const r1: number

const r2 = either(succ, fail, { success: _false, cause: 404 });
// const r2: string

const r3 = either(succ, fail,
    Math.random() < 0.5 ?
        { success: _true, result: "abc" } :
        { success: _false, cause: 404 }
);
// const r3: string | number

看起来不错,所以either人中至少有callers人有同样的经历,呃,如果你忽略布尔的东西.


这就对了.就我个人而言,我可能只会坚持类型断言,也许在future 的TypeScrip版本中,当控制流分析影响泛型类型参数时,您可以删除它们.

Playground link to code

Typescript相关问答推荐

在TypScript手册中可以视为接口类型是什么意思?

如果在TypeScript中一个原始的类方法是递归的,如何使用类decorator 模式?

在Angular中,如何在文件上传后清除文件上传文本框

如果存在ts—mock—imports,我们是否需要typescpt中的IoC容器

基于平台的重定向,Angular 为16

TypeScrip 5.3和5.4-对`Readonly<;[...number[],字符串]>;`的测试版处理:有什么变化?

访问继承接口的属性时在html中出错.角形

Angular NgModel不更新Typescript

函数重载中的不正确TS建议

类型脚本可以推断哪个类型作为参数传递到联合类型中吗?

从泛型类型引用推断的文本类型

vue-tsc失败,错误引用项目可能无法禁用vue项目上的emit

如何过滤文字中的类对象联合类型?

打字脚本中的模块化或命名空间

使用Type脚本更新对象属性

在TypeScript中,如何通过一系列过滤器缩小类型?

为什么过滤器不改变可能的类型?

覆盖高阶组件中的 prop 时的泛型推理

为什么 typescript 在对象合并期间无法判断无效键?

通用可选函数参数返回类型