Typescript: Suggestion: Allow interfaces to "implement" (vs extend) other interfaces

Created on 19 May 2018  ·  11Comments  ·  Source: microsoft/TypeScript

Search Terms

interface, implements

Suggestion

Allow declaring that an interface "implements" another interface or interfaces, which means the compiler checks conformance, but unlike the "extends" clause, no members are inherited:

interface Foo {
  foo(): void;
}

interface Bar implements Foo {
  foo(): void; // must be present to satisfy type-checker
  bar(): void;
}

Use Cases

It is very common for one interface to be an "extension" of another, but the "extends" keyword is not a universal way to make this fact explicit in code. Because of structural typing, the fact that one interface is assignable to another is true with or without "extends," so you might say the "extends" keyword serves primarily to inherit members, and secondarily to document and enforce the relationship between the two types. Inheriting members comes with a readability trade-off and is not always desirable, so it would be useful to be able to document and enforce the relationship between two interfaces without inheriting members.

Consider code such as:

import { GestureHandler } from './GestureHandler'
import { DropTarget } from './DropTarget'

export interface DragAndDropHandler extends GestureHandler {
  updateDropTarget(dropTarget: DropTaget): void;
}

function createDragAndDropHandler(/*...*/): DragAndDropHandler {
  //...
}

While this code is not bad, it is notable that DragAndDropHandler omits some of its members simply because it has a relationship with GestureHandler. What are those members? What if I would like to declare them explicitly, just as I would if GestureHandler didn't exist, or if DragAndDropHandler were a class that implemented GestureHandler? I could write them in, but the compiler won't check that I have included all of them. I could omit extends GestureHandler, but then the type-checking will happen where DragAndDropHandler is used as a GestureHandler, not where it is defined.

What I really want to do is be explicit about — _and have the compiler check_ — that I am specifying all members of this interface, and also that it conforms to GestureHandler.

I would like to be able to write:

export interface DragAndDropHandler implements GestureHandler {
  updateDropTarget(dropTarget: DropTaget): void;
  move(gestureInfo: GestureInfo): void
  finish(gestureInfo: GestureInfo, success: boolean): void
}

Examples

See above

Checklist

My suggestion meets these guidelines:

  • [X] This wouldn't be a breaking change in existing TypeScript / JavaScript code
  • [X] This wouldn't change the runtime behavior of existing JavaScript code
  • [X] This could be implemented without emitting different JS based on the types of the expressions
  • [X] This isn't a runtime feature (e.g. new expression-level syntax)
Awaiting More Feedback Suggestion

Most helpful comment

@ArmorDarks I think you can achieve the effect you want without any changes to typescript. You can have a type alias as the extends clause. The type alias can check if the currently declared interface extends the wanted interface in its type parameters but the actual result of this type alias will never add anything to the interface (it will be {}).

interface A {
    foo: string;
    bar: number
}

type Implements<T, U extends T> = {}

interface B extends Implements<A, B> {
    foo: string;
    bar: number
}

Usually unused type parameters are a bad idea, but I think we can at least rely on the fact that the type parameter constrains will be checked on instantiation of the type alias even if they are not used afterwards (although I am ready to be contradicted on that by anyone with more insight into this).

The disadvantage of this is that you have to be aware that this exists and of its semantics, but ultimately using implements would have the same drawbacks and the implements solution might actually confuse a lot of people when they mistakenly use implements and get different results then they would expect.

All 11 comments

It would probably be possible to write a custom tslint rule to check that an interface redeclares all inherited members.

Can you elaborate on the value checking the interface declaration gives you vs checking the actual use? in the example above, you would get an error in createDragAndDropHandler if the implementation did not conform to GestureHandler.

it would be useful to be able to document and enforce the relationship between two interfaces without inheriting members.

The value of having the choice to write interface Foo implements Bar for some interface declarations, rather than interface Foo extends Bar, is 1) in some cases it reads much better to declare all members of Foo, not just the ones absent in Bar, and 2) I may not want to automatically inherit members that are later added to Bar! It would be a compiler error if someone adds a member to Bar that isn't in Foo.

To give another example, consider the difference between:

interface Point3D extends Point {
  z: number;
}

and:

interface Point3D implements Point {
  x: number;
  y: number;
  z: number;
}

The value of the latter is 1) It's more readable (especially if Point is in another file or package), and 2) if someone adds a member to Point, it doesn't automatically get added to Point3D. Instead, it becomes a conformance error.

Edit: In code today you could also write:

interface Point3D extends Point {
  x: number;
  y: number;
  z: number;
}

However, the compiler will not guarantee that this is a complete specification of Point3D (so you still have to go look up Point if you want the complete list of members), and (2) remains (potentially unwanted inheritance of members added to Point in the future).

What if an _optional_ member is added to Point? With the proposed implements feature, conformance holds and nothing more happens. With extends, Point3D has gained a new member, which may or may not make sense. It probably won't make sense if the author of Point3D was using extends merely to annotate conformance, not for inheritance. Then the declaration of Point3D must be updated to explicitly override the unwanted optional property.

Actually, inheriting unwanted optional members in interfaces is a problem in general, so let's promote it to benefit (3): When extending an interface with optional properties, you must explicitly "stamp out" all current and future properties you don't want, I think by writing foo?: never. When _implementing_ an interface with an interface (under the proposal), you can just list the ones you want.

Especially across module boundaries, implementing an interface is safer than inheriting from one.

And just having an interface Point3D that does not extends or implements Point, and get the error on the first invalid use, instead of on the interface?

@mhegazy You could say the same thing about class Foo implements Bar and just get rid of the implements keyboard altogether, since structurally the class either conforms to the interface anyway or it doesn't. If it's important that a class implements an interface, implements allows you to write this explicitly and guarantee that conformance will be checked.

class Foo implements Bar and just get rid of the implements keyboard altogether

Actually, I would rather we did :D

Heh. It's possible that the demand for this specific feature doesn't justify the cost. However, I don't agree with "just catch it at the use site" as the right attitude here, and in general, I am in favor of providing ways to tell the compiler that one type is _supposed_ to be assignable to another, such that it will throw an error if not. Sometimes the way you have to construct a subtype of X (e.g. via an intersection type, or all sorts of ways) does not obviously produce a subtype of X. Relying on use sites is similar in spirit to relying on runtime: you'll get an error anyway, later, _if your code exercises that case_, so why do you need a better error, sooner, that you are certain to see? Why bother to explicitly write out the type you expect something to be?

For values, I'm already using the incantation const n: never = x to "assert" that x is assignable to never (e.g. to make a checked-exhaustive if-else). I guess there is a similar incantation for types, something like putting this at the top level on a line by itself: (p: Point3D): Point => p;.

I'm here with the same question.

At first, I thought that intersection can help enforce that confrontation, but I was wrong.

I think that use case it pretty much valid and basically the same as for class implements — it's needed to explicitly say, that newly declared interface should always implement another interface (but it can be broader).

I think the question isn't about implements. It's about finding a way to declare the fact, that some type _at least_ should properly implement another type. It just happens that implements seem to be a natural fit for it.

@ArmorDarks I think you can achieve the effect you want without any changes to typescript. You can have a type alias as the extends clause. The type alias can check if the currently declared interface extends the wanted interface in its type parameters but the actual result of this type alias will never add anything to the interface (it will be {}).

interface A {
    foo: string;
    bar: number
}

type Implements<T, U extends T> = {}

interface B extends Implements<A, B> {
    foo: string;
    bar: number
}

Usually unused type parameters are a bad idea, but I think we can at least rely on the fact that the type parameter constrains will be checked on instantiation of the type alias even if they are not used afterwards (although I am ready to be contradicted on that by anyone with more insight into this).

The disadvantage of this is that you have to be aware that this exists and of its semantics, but ultimately using implements would have the same drawbacks and the implements solution might actually confuse a lot of people when they mistakenly use implements and get different results then they would expect.

@dragomirtitian thanks for the suggested solution. I'm struggling with a similar issue that I'm quite sure is related to this thread.
To use your example, what if I have the following:

interface A {
    [key: string]: {
         foo: string;
    }
}

type Implements<T, U extends T> = {}

interface B extends Implements<A, B> {
    bar: {
        foo: string;
    }
}

Typescript is throwing an error where Index signature is missing in type, but what I would expect is for it to understand that bar is a key string in that case

@dimabru The error is not wrong. You would get the same error for a class implementing such an interface (class BB implements A {} causes Index signature is missing in type 'BB').

If you want to ensure all the interface are of the same type, this would work:

interface A {
    [key: string]: {
         foo: string;
    }
}

type Implements<T, U extends T> = {}

interface B extends Implements<Record<keyof B, A[string]>, B> {
    bar: {
        foo: string;
    }
}

Or a more encapsulated version:

type ConstrainValues<TItem, T extends Record<keyof T, TItem>> = {}

interface B2 extends ConstrainValues<A[string], B2> {
    bar: {
        foo: string;
    }
}

Playground Link

Was this page helpful?
0 / 5 - 0 ratings

Related issues

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

Roam-Cooper picture Roam-Cooper  ·  3Comments

manekinekko picture manekinekko  ·  3Comments

bgrieder picture bgrieder  ·  3Comments

fwanicka picture fwanicka  ·  3Comments