1

Goal

I'm optimally trying to just enforce the typing in the project so that anyone that implements an Animal must provide this function and ensure that the function can only take either a Woof or a Meow as the sole parameter and must return only either a Woof or Meow.

Initial Attempt

Here's some sample code that's similar (except in names) to the code I've got in VSCode (using TypeScript 2.4.1):

class Woof {
    constructor(private a: boolean = true) {}
}

class Meow {
    constructor(private b: string = "abc") {} 
}

class Dog implements Animal  {
    doSomething(sound: Woof): Woof {
        return new Woof();
    }
}

class Cat implements Animal {
    doSomething(sound: Meow): Meow {
        return new Meow();
    }
}

interface Animal {
    doSomething: <T extends Woof | Meow>(input: T) => T; 
}

If I drop it in the TypeScript playground, it yields the JavaScript I'd expect without error.

However, within VSCode, I've got a red squiggly line under both the Dog and Cat classes and the mouseover reads all of the following:

Class 'Dog' incorrectly implements interface 'Animal'.

Types of property 'doSomething' are incompatible.

Type '(sound: Woof) => Woof' is not assignable to a type '(input: T) => T'.

Types of parameters 'sound' and 'sound' are incompatible.

Type 'T' is not assignable to type 'Woof'.

Type 'Woof | Meow' is not assignable to type 'Woof'.

Type 'Meow' is not assignable to type 'Woof'.

Property 'a' is missing in type 'Meow'

Each of the errors is nested, so it's difficult to display the formatting accurately here, but it appears to indicate that I can't just specify either a Woof or Meow type when extending the generic because the private properties aren't implemented across both types (which looks similar to another question here). This is despite not seeing anything in the documentation that would indicate that this is illegal (and again, in the playground, this appears to work without issue). But, unlike that question, I don't want the generic to extend both types, but rather be constrained to one or the other.

Reading the description in a post about intersection types, it reads "A union type A | B represents an entity that is either of type A or type B, whereas an intersection type A & B represents an entity that is both type A and type B". Reading that, I definitely want a union type, but that's what I appear to have above, and it isn't appearing to take.

The types are flipped in the squigglies under the alternate class, but the message is the same.

I also get this error when I actually build my project with 'tsc' in the output.

Alternate attempt - Replacing the types with an interface each implements

I've also tried implementing the following in hopes that it would eliminate that bizarre need for the private properties to match between the Woof and Meow classes (in case something unexpected was just not working when specifying each):

interface Sound {
}

class Woof implements Sound {
    constructor(private a: boolean = true) {}
}

class Meow implements Sound {
    constructor(private b: string = "abc") {} 
}

class Dog implements Animal  {
    doSomething(sound: Woof): Woof {
        return new Woof();
    }
}

class Cat implements Animal {
    doSomething(sound: Meow): Meow {
        return new Meow();
    }
}

interface Animal {
    doSomething: <T extends Sound>(input: T) => T; 
}

I get exactly the same error as before for each of the classes.

Semi-working Version

Now, if I just eliminate the generics altogether and use only the interface from the alternative above, it solves part of the goal in that I can restrict the parameter and the result to a class implementing the interface, but that's not exactly ideal since it doesn't prevent another developer from passing in one interface implementation and attempting to return another.

interface Sound {
}

class Woof implements Sound {
    constructor(private a: boolean = true) {}
}

class Meow implements Sound {
    constructor(private b: string = "abc") {} 
}

class Dog implements Animal  {
    doSomething(sound: Woof): Woof {
        return new Woof();
    }
}

class Cat implements Animal {
    doSomething(sound: Meow): Meow {
        return new Meow();
    }
}

interface Animal {
    doSomething: (input: Sound) => Sound; 
}

I'd prefer the type safety of using generics and limiting T to one implementation. Any ideas about how I can go about this using generics with a syntax that Typescript would actually work with outside of the playground?

Whit Waldo
  • 4,806
  • 4
  • 48
  • 70
  • 1
    How about using overloads instead of generics? – unional Jul 09 '17 at 05:52
  • @unional I'd rather not, given that I want other developers to simply be constrained (via the type system) to implement against the Animal interface and be constrained to input one type and output the same type, with each specific option right there in Animal's function definition itself. While I could just union the types, it seems odd that I can't just incorporate those constraints on the generic itself. – Whit Waldo Jul 09 '17 at 06:00

1 Answers1

2

Would this work for you?

class Woof {
  constructor(private a: boolean = true) { }
}

class Meow {
  constructor(private b: string = "abc") { }
}

class Dog implements Animal<Woof> {
  doSomething(sound: Woof): Woof {
    return new Woof();
  }
}

class Cat implements Animal<Meow> {
  doSomething(sound: Meow): Meow {
    return new Meow();
  }
}

interface Animal<T extends Meow | Woof> {
  doSomething(input: T): T;
}
unional
  • 14,651
  • 5
  • 32
  • 56
  • Works great - why does that eliminate the error to put the constraint in the interface, but not on the functions themselves? – Whit Waldo Jul 09 '17 at 06:08
  • 1
    Because on the function you rely on the compiler to infer the type of T. During inferring I think it can't narrow the type. Maybe by design or some improvement can be made. The solution I give just make it explicit. – unional Jul 09 '17 at 06:16