Typescript使用鉴别器的联合字符串类型创建ADT(区分联合类型)的实例

时间:2017-10-30 15:05:22

标签: typescript

让我们考虑以下代码。我有一个ADT,其中具体类型之间的差异是kind属性。我想有一个实用程序方法来创建这些类的实例。

interface I {
    id: number;
}

interface A extends I {
    kind: "a";
}

interface B extends I {
    kind: "b";
}

type O = A | B;

function create(id: number, kind: "a" | "b"): O {
    return {
        id: 1,
        kind: kind
    };
}

显然上面的代码会出错,因为"a" | "b"无法分配给"a""b"。反正有没有表达这个功能?以某种方式表达类型oneOf "a" | "b"

2 个答案:

答案 0 :(得分:2)

更新:2019-05-30随着TypeScript 3.5的发布,这应由smarter union type checking解决。以下内容适用于3.4及以下:

我认为您已正确表达了代码,而且TypeScript阻止了有效的分配。你和我很明显O应该在结构上与

相同
type OPrime = {
  id: number;
  kind: "a" | "b"
}

由于create()返回OPrime类型的值,因此应将其分配给O。唉,TypeScript认为O可分配给OPrime,但反之亦然。

最简单的解决方法是使用类型断言:

function create(id: number, kind: "a" | "b"): O {
    return {
        id: 1,
        kind: kind
    } as O; // assertion
}

类型断言通常不安全,但是当您知道的不仅仅是编译器可以弄清楚表达式的类型时,它们也很有用。但是很难区分编译器警告你的合法错误,以及编译器无法识别有效表达式。在这种情况下,我很确定你做对了,编译器报告错误的误报。

对于TypeScript,结果是intended behavior。虽然OOPrime恰好相同,但编译器需要额外的工作来识别它。并且通常这是not warranted,因为两个联合成分很少仅仅通过单个同名属性的类型来区分。在大多数情况下(甚至可能是您的实际非玩具用例),您的AB类型中会包含其他类型。例如:

interface AExtra extends I {
    kind: "a";
    name: string;
}

interface BExtra extends I {
    kind: "b";
    age: number;
}

type OExtra = AExtra | BExtra;

现在不再可能将OExtra表示为具有联合值属性的单个非联合类型。考虑OExtraPrime

type OExtraPrime = {
  id: number;
  kind: "a" | "b";
  name?: string;
  age?: number;
}

确实,任何OExtra都可以分配给OExtraPrime,但反之亦然。因此,在一般实践中,OOPrime之间的等价性没有出现,编译器也不愿意建立它。所以你能做的最好的就是类型断言。

希望这会有所帮助;祝你好运!

答案 1 :(得分:0)

可以使用overload declarations键入此类函数。您可以将所需类型作为重载之一,其他人在静态知道kind参数时声明更具体的返回类型。重载函数实现中的实际返回类型更少(它仅用于检查实现,而不是调用站点),因此它可以只是any,或者它可以是与实现兼容的任何方便类型和所有重载声明。

interface I {
    id: number;
}

interface A extends I {
    kind: "a";
}

interface B extends I {
    kind: "b";
}

type O = A | B;


function create(id: number, kind: "a"): A;
function create(id: number, kind: "b"): B;
function create(id: number, kind: O["kind"]): O;
function create<K extends O["kind"]>(id: number, kind: K): { id: number; kind: K } {
    return {
        id,
        kind
    };
}

function testA(id: number) {  // inferred as function testA(id: number): A
    return create(id, "a");
}
function test(id: number, kind: "a" | "b") { 
// inferred as function test(id: number, kind: "a" | "b"): O
    return create(id, kind);
}