rdiddly
rdiddly

Reputation: 383

Typescript class declaration syntax with generics

Given the following:

interface Component {}
interface Composite<component extends Component> {
  getChildren(): component[];
}
class StackSegment implements Component {}
class Stack implements Composite<StackSegment> {
    getChildren(): StackSegment[] {
        return new Array<StackSegment>();
    }
}

Why does the following cause a compiler error?

class Bar<component extends Component = StackSegment, 
       composite extends Composite<component> = Stack> {
}

The error that I get (on Stack as the default value of composite) says that StackSegment is not assignable to type 'component'.

Upvotes: 0

Views: 499

Answers (3)

jcalz
jcalz

Reputation: 328302

Note: in what follows I'm using short uppercase identifiers for type parameters, as the normal convention. You can replace CN with component and CS with composite at your pleasure (but I'd recommend against using regular identifiers for generic type parameters)


I'm not sure what your use cases are, but default type parameters don't set constraints. In your code,

class Bar<CN extends Component = StackSegment, 
       CS extends Composite<CN> = Stack> {  // error
}

The type parameter CN is not required to be StackSegment, and thus it's possible for Stack not to meet the constraint of Composite<CN>. One way to deal with it is to make the composite just Composite<CN> instead of Stack:

class Bar<CN extends Component = StackSegment, 
       CS extends Composite<CN> = Composite<CN>> {  // okay
}

If you really want to see the default as Stack (e.g., if Stack has some extra methods that just Composite<CN> doesn't), then you can do this:

class Bar<CN extends Component = StackSegment, 
       CS extends Composite<CN> = Stack & Composite<CN>> {  // okay
}

since Stack & Composite<StackSegment> is the same structural type as Stack. But again, I don't know your use case. As of now, you get stuff like:

interface OtherComponent extends Component {
    thingy: string;
}

class OtherComposite implements Composite<OtherComponent> {
    getChildren(): OtherComponent[] {
        throw new Error("Method not implemented.");
    }
}

new Bar(); // Bar<StackSegment, Stack> as desired
new Bar<OtherComponent, OtherComposite>(); // also makes sense

// but this is a Bar<OtherComponent, Stack & Composite<OtherComponent>>
new Bar<OtherComponent>(); // wha?
// which is weird.

Do you ever intend to use just one default parameter? If not, maybe there's a better way to represent the generics. But without more use case information I wouldn't know how to advise you.

Good luck.


EDIT: An ugly syntax that works the way I think you want is to specify both types in one parameter, like this:

class Bar<CC extends [Component, Composite<CC[0]>]=[StackSegment, Stack],
    CN extends CC[0] = CC[0],
    CS extends CC[1] = CC[1]> {    
}

In the above, CC is a two-tuple of types with the constraint you want (the second parameter must be compatible with Composite<> of the first parameter), and it defaults to the pair [StackSegment, Stack] as desired. (The CN and CS types are just there for convenience so you don't need to use CC[0] for the component type and CC[1] for the composite type).

Now the behavior is something like this:

new Bar(); // CN is StackSegment and CS is Stack, as desired
new Bar<[OtherComponent, OtherComposite]>(); // also makes sense

But you can't easily break it like before:

new Bar<[OtherComponent]>(); // error
new Bar<[OtherComponent, Stack]>(); // error

Okay, good luck again!

Upvotes: 1

rdiddly
rdiddly

Reputation: 383

Changing the line of code that causes the compiler error as follows:

class Bar<component extends Component = StackSegment, 
    composite extends Composite<Component> = Stack> {}  

eliminates the error.

Now the question is, will it work as expected when overridden.

Upvotes: 0

Oscar Paz
Oscar Paz

Reputation: 18292

The problem, I think, is that the second type parameter depends on the first. I mean, if the first parameter is of type P then the second must be of type Composite<P> (assuming P implments Component).

But as you are assigning default types, you could do this:

let b = new Bar<P>();

Here I don't pass a second type parameter, so TypeScript assumes it is Stack. However, Stack does not extend P.

A default value for a type parameter not always enforces the type constraints, and so such a type cannot be declared.

Upvotes: 1

Related Questions