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?