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
type
is 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.