Skip to content

Exhaustiveness checking with classes #20512

@mattroberts297

Description

@mattroberts297

I'm not sure if this is a bug or a suggestion as the documentation only talks about types (as opposed to classes) and I can understand not permitting discriminated unions, also known as tagged unions or algebraic data types that involve classes. I am coming from Scala which supports it, but each language is different and I guess you may be limited by Javascript. Here's some code:

TypeScript Version: 2.6.0

Code

Maybe.ts

export class Some<A> {
  readonly kind: "some";
  readonly value: A;

  constructor(value: A) {
    this.value = value;
  }

  then<B>(callback: (a: A) => B): Maybe<B> {
    return new Some<B>(callback(this.value)); // todo factory
  }
}

export class None<A> {
  readonly kind: "none";
  constructor() { }

  then<B>(callback: (a: A) => B): Maybe<B> {
    return new None<B>();
  }
}

export type Maybe<A> = Some<A> | None<A>;

export function print<A>(m: Maybe<A>) {
  switch (m.kind) {
    case "some": return `Some(${m.value})`;
    case "none": return "None";
  }
}

Maybe.spec.ts

import { print, Maybe, Some, None } from "./Maybe";
import "mocha";
import { expect } from "chai";

describe("maybe", () => {
  it("should permit exhaustive checking", () => {
    expect(print(new Some(10))).to.equal("Some(10)");
    expect(print(new None<number>())).to.equal("None");
  });
});

Expected behavior:

Test passes

  maybe
    ✓ should permit exhaustive checking

Actual behavior:

Test fails

  1) maybe should permit exhaustive checking:
     AssertionError: expected undefined to equal 'Some(10)'
      at Context.it (lib/Maybe.spec.js:8:63)

Work around code

Maybe.ts

export interface Some<A> {
  readonly kind: "some";
  readonly value: A;
}

export interface None {
  readonly kind: "none";
}

export type Maybe<A> = Some<A> | None;

export function print<A>(m: Maybe<A>) {
  switch (m.kind) {
    case "some": return `Some(${m.value})`;
    case "none": return "None";
  }
}

export function maybe<A>(a: A | null | undefined): Maybe<A> {
  if (a == null || a == undefined) {
    return { kind: "none" };
  } else {
    return { kind: "some", value: a};
  }
}

// todo then<A, B>(a: Maybe<A>, map: A => B): Maybe<B>

Maybe.spec.ts

import { print, Maybe, maybe } from "./Maybe";
import "mocha";
import { expect } from "chai";

describe("maybe", () => {
  it("should permit exhaustive checking", () => {
    expect(print(maybe(10))).to.equal("Some(10)");
    expect(print(maybe<number>(null))).to.equal("None");
  });
});

Thanks for the fun, manageable language.

Metadata

Metadata

Assignees

No one assigned

    Labels

    QuestionAn issue which isn't directly actionable in code

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions