Consider the following program using phantom types:
const strlen = (str: string) => str.length;
type Const<A, B> = { type: 'Const', value: A };
const Const = <A, B = never>(value: A): Const<A, B> => ({ type: 'Const', value });
const map = <A, B, C>(_f: (value: B) => C, { value }: Const<A, B>): Const<A, C> => Const(value);
const contramap = <A, B, C>(_f: (value: C) => B, { value }: Const<A, B>): Const<A, C> => Const(value);
const constant = Const(true);
map(strlen, constant); // works
contramap(strlen, constant); // works
The above program type checks because the correct types are inferred. The constant
value has the inferred type Const<boolean, never>
. The map
function is called with the types A = boolean
, B = string
, and C = number
. The contramap
function is called with the types A = boolean
, B = number
, and C = string
.
However, it would be nice to write the above expressions using methods instead of functions. Hence, I tried the following:
const strlen = (str: string) => str.length;
interface Const<A, B> {
map: <C>(f: (value: B) => C) => Const<A, C>;
contramap: <C>(f: (value: C) => B) => Const<A, C>;
}
const Const = <A, B = never>(value: A): Const<A, B> => ({
map: () => Const(value),
contramap: () => Const(value)
});
const constant = Const(true);
constant.map(strlen); // works
constant.contramap(strlen); // error
As you can see, the map
method works but the contramap
method doesn't. This is because the type of constant
is Const<boolean, never>
and it's not refined by the method call, i.e for map
the type is not refined to Const<boolean, string>
and for contramap
the type is not refined to Const<boolean, number>
.
Because of this, either map
or contramap
work but not both. If the type of the object is Const<boolean, never>
then contramap
doesn't work. If the type of the object is Const<boolean, unknown>
then map
doesn't work.
How can I make both map
and contramap
work using methods instead of functions?
I solved this problem by making the type parameter B
, of the Const
interface, a phantom type.
const strlen = (str: string) => str.length;
interface Const<A, B> {
map: <B, C>(f: (value: B) => C) => Const<A, C>;
contramap: <B, C>(f: (value: C) => B) => Const<A, C>;
}
const Const = <A, B = never>(value: A): Const<A, B> => ({
map: () => Const(value),
contramap: () => Const(value)
});
const constant = Const(true);
constant.map(strlen); // works
constant.contramap(strlen); // works
The type parameter B
, of the Const
interface, is now shadowed by the type parameters of map
and contramap
. This makes sense because the type parameter B
, of the Const
interface, is a phantom type. Hence, it shouldn't be used. On the other hand, the callers of map
and contramap
should be able to decide what type the type parameter B
should be instantiated with.