Delving into the Typescript type system, I am currently working on implementing the Fantasy Land Spec and encountered a hurdle while trying to implement the specification for Semigroup.
The spec dictates that a Semigroup
must follow the type definition outlined below:
concat :: Semigroup a => a ~> a -> a
Interpreting this, I understand that a type a
, which implements Semigroup
, should have a concat
method that takes a parameter of type a
and returns a value of type a
.
The way I attempted to express this type definition in TypeScript is as follows:
interface Semigroup {
concat(other: this): this;
}
However, when trying to implement this interface in a class, like so:
class Sum implements Semigroup {
constructor(readonly num: number) {}
concat(other: Sum): Sum {
return new Sum(this.num + other.num);
}
}
I encountered a compiler error stating:
Property 'concat' in type 'Sum' is not assignable to the same property in base type 'Semigroup'.
Type '(other: Sum) => Sum' is not assignable to type '(other: this) => this'.
Type 'Sum' is not assignable to type 'this'.
'Sum' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Sum'.(2416)
Upon reading this answer on Stack Overflow, I gained insight into the issue.
It seems the compiler is essentially informing me that while the interface specifies taking a parameter of the concrete type this
(Sum
in this case), it could also accept a class extending Sum
.
Yet, I am uncertain about how to address this issue. Specifically, I am unsure about how to articulate the type definition for Semigroup
in TypeScript. How can the implementing class be referenced from an interface?
For a demonstration, please visit the TS Playground.
Update
Reflecting on @Guerric P's solution, I believe it offers a partial remedy. Guerric suggested utilizing a generic within the interface. While this solution facilitates the implementation of the Semigroup
specification, as evidenced here, the interface itself does not strictly enforce it.
Moreover, the fantasy land specification elaborates on the criteria as follows:
s.concat(b)
/**
* `b` must be a value of the same `Semigroup`
*
* If `b` is not the same semigroup, behaviour of `concat` is
* unspecified.
*
* `concat` must return a value of the same `Semigroup`.
*/
Considering making b
a generic, my proposal moves towards restricting the type to Semigroup
. This adjustment enforces the constraint that b
must be of type Semigroup
, as showcased here:
interface Semigroup {
concat(other: Semigroup): Semigroup;
}
However, the specification still falls short of mandating that b
must belong to the SAME Semigroup
. I am still seeking a solution within the TypeScript type system to achieve this level of specificity.