Here are the different ways of creating an algebraic datatype or a tagged value.
type Type = 'A' | 'B';
type Value = any;
type ADTTuple = [Type, Value];
type ADTNamed = { type: Type } & {};
type ADTObject = { type: Type, value: Value };
type ADTKey = {
[K in Type]: {[P in K]: Value}
}[Type];
function matchTuple(x: ADTTuple) {
const [type, value] = x;
if (type === 'A') {
// ... value
} else if (type === 'B') {
// ... value
}
}
function matchNamed(x: ADTNamed) {
const { type, ...value } = x;
if (type === 'A') {
// ... value
} else if (type === 'B') {
// ... value
}
}
function matchObject(x: ADTObject) {
const { type, value } = x;
if (type === 'A') {
// ... value
} else if (type === 'B') {
// ... value
}
}
function matchKey(x: ADTKey) {
if ('A' in x) {
// ... x['A']
} else if ('B' in x) {
// ... x['B']
}
}The ADTNamed is the most inflexible due to 2 reasons:
- Name clashes because
typeis a special keyword now. - It limits the value to an object type, it cannot be just a number or other primitive as the intersection won't work.
I prefer the ADTTuple, ADTObject and ADTKey forms.
The ADTTuple is nice for simple internal data structures. It's easy to destructure and work with. It is however not very descriptive, nor self-documenting.
The ADTObject and ADTKey are very similar. The main difference is that the ADTKey is more succinct.
This can be useful for wrappers that may result in repeated usage like value.value that occurs with ADTObject.
An example of a good use of ADTKey is JSON-RPC 2.0:
{
"jsonrpc": "2.0",
"id": null,
"result": null
}{
"jsonrpc": "2.0",
"id": null,
"error": {
"code": -32600,
"message": "..."
}
}The usage of the key result vs error is what determines the 2 types of messages.