简体   繁体   中英

In TypeScript, how do you express that a type T, declared inside an interface I, should be of whatever type U that will implement said interface I

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);
    }
}

TypeScript playground

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM