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
✓ 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.
Here
readonly kind: "none";
You need to write this
readonly kind: "none" = "none";
The former only declares that there is a kind property with value "none" without actually initializing it to a value.
The minimal example to play with is
class A {
kind: "a" /* = "a" */;
}
var x = new A();
console.log(x.kind); // undefined, not "a"
This is a runtime error, not a compile-time one. The problem is that the kind field is never assigned to. You have provided a type for it, but not a value. This mistake is now caught by the latest compiler (typescript@next) if you turn --strictNullChecks and --strictPropertyInitialization on.
export class Some<A> {
readonly kind: "some";
readonly value: A;
constructor(value: A) {
this.value = value;
this.kind = "some"; // fix in the constructor
}
...
}
export class None<A> {
readonly kind: "none" = "none"; // .. or inline
...
}
Thank you @RyanCavanaugh and @gcnew. Really appreciate you taking the time to answer even though this turned out to be a question (not a bug or suggestion). I'm up and running now.
This mistake is now caught by the latest compiler (typescript@next) if you turn --strictNullChecks and --strictPropertyInitialization on.
Amazing that you're also guarding against developer incompetence in the compiler - thanks again!
Most helpful comment
Here
You need to write this
The former only declares that there is a
kindproperty with value"none"without actually initializing it to a value.The minimal example to play with is