Skip to content

Exhaustiveness checking with classes #20512

Closed
@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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions