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 theConstinterface, a phantom type.Playground
The type parameter
B, of theConstinterface, is now shadowed by the type parameters ofmapandcontramap. This makes sense because the type parameterB, of theConstinterface, is a phantom type. Hence, it shouldn't be used. On the other hand, the callers ofmapandcontramapshould be able to decide what type the type parameterBshould be instantiated with.