I'm exploring the Typescript type system by implementing the Fantasy Land Spec and I ran into an issue while trying to implement the spec for Semigroup .
The spec stipulates that a Semigroup
should adhere to the following type definition:
concat :: Semigroup a => a ~> a -> a
I understand this to mean that a type a
, which implements Semigroup
, should have a concat
method that takes in a parameter of type a
and returns a parameter of type a
.
The only way I could think of expressing this type definition in TypeScript is this:
interface Semigroup {
concat(other: this): this;
}
But when I try to implement this interface on a class, like this:
class Sum implements Setoid, Semigroup {
constructor(readonly num: number) {}
concat(other: Sum): Sum {
return new Sum(this.num + other.num);
}
}
I get a compiler error telling me that:
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)
Thanks to this S/O answer, I think I understand the problem.
I think the compiler is essentially telling me: your interface says that you should be taking a parameter that is of the concrete type this
( Sum
in this particular case), but a class that extends Sum
could also be passed in.
However, I don't know how to fix it. That is, I don't know how to express the type definition for Semigroup
in TypeScript. How does one express that a type T, declared inside an interface I, should be of whatever type U that will implement said interface I?
Here is link to a TS Playground.
I don't want to question your interpretation of that fantasy-land spec, which I admit I don't fully understand, so I'll assume your interpretation is right.
The problem is that your class
could be extended, so this
could refer to that extended class. There is no such thing as final class
or equivalent in TypeScript.
Now let's suppose you have an ExtendedSum
class which extends Sum
. Your equals
implementation still works because (other: Sum) => boolean
is assignable to (other: ExtendedSum) => boolean
. Indeed, a function that takes a Sum
as parameter can take an ExtendedSum
as well (structural typing principle).
However, your concat
implementation doesn't work because (other: Sum) => Sum
is not assignable to (other: ExtendedSum) => ExtendedSum
. Indeed, a function that returns a Sum
is not assignable to a function that returns an ExtendedSum
because a Sum
is not necessarily an ExtendedSum
.
You could fix that with a generic typed interface:
interface Semigroup<T> {
concat(other: T): T;
}
class Sum implements Setoid, Semigroup<Sum> {
constructor(readonly num: number) {}
equals(other: Sum): boolean {
return this.num === other.num;
}
concat(other: Sum): Sum {
return new Sum(this.num + other.num);
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.