Typescript: Selectively hiding interface methods

Created on 19 Mar 2015  路  9Comments  路  Source: microsoft/TypeScript

Motivation

Assume a generic HTML input element modelled by the following interface:

interface Input<TValue> {

    getValue() : TValue;

    setValue(value: TValue): void;

    clear(): void;

    onClick: Event;

    //etc...
}

We want to create a derivation specialised to string

interface TextInput extends Input<string> {

     // We specifically want this method to be called `setText`,
     //  since it is felt to be more user-friendly than `setValue`
     setText(text: string) : void;
}

We now have a problem. The user needs to refer to the documentation in order to decide which method they should use:

var textInput : TextInput;

textInput.setValue('foo');  // or
textInput.setText('foo'); // ???

We effectively would like to rename the method (or change its signature), rather than creating a new one.

Proposal A

Introduce syntax - [identifier]:

interface TextInput extends Input<string> {

     - setValue(value: number): void; // Error: invalid reference to inherited method
     - setValue2(value: string): void; // Error: invalid reference to inherited method
     - setValue(value: string): void; // Okay

     setText(text: string) : void;
}

var textInput : TextInput;
textInput.setValue('foo');  // Error: method does not exist
textInput.setText('foo');  // Okay

The use-case above is slightly contrived, but the usefulness of providing more fine-grained control over interface declarations is hopefully clear.

To ensure casting is safe, we should require types that implement an interface also implement any hidden methods:

class TextInputClass implements TextInput {

   public setValue(value: string) { /**/ }  // Required

   public setText(text: string) {
       this.setValue(text):
   }
}

Proposal B

Introduce an @obsolete decorator.

interface TextInput extends Input<string> {

     @obsolete
     setValue(value: string): void;

     setText(text: string) : void;
}

This does the following:

  • Removes the target item from completion lists.
  • Generates a compiler warning if the item is used.
Out of Scope Suggestion

Most helpful comment

This issue should be marked as fixed.

type StringLiteralDiff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = Pick<T, StringLiteralDiff<keyof T, K>>;

interface Input<TValue> {

    getValue(): TValue;
    setValue(value: TValue): void;
    clear(): void;
}

interface TextInput extends Omit<Input<string>, 'setValue'> {

    setText(text: string): void;
}

let x = {} as TextInput;
x.setValue('s'); // We get the expected error: Property setValue does not exist on type 'TextInput' 

Although, older and wiser now, I wouldn't recommend anyone actually doing that!

All 9 comments

Having a sub-type that has less members than the parent type really breaks OO though. Maybe you can turn this into a proposal for a @Deprecated annotation in the upcoming 1.5?

@billccn, I agree that this is uncharted territory as far as OO goes. But it's possible to do a lot of weird things in JavaScript.

Also I don't see this as deprecation. Let's assume we are in full control and don't care about breaking changes etc. This is more about fine-tuning the API for certain classes so that they are easy to use and understand.

Do you have a specific example where this breaks OO in the _structural sense_? Note that the underlying object will contain all hidden methods.

One of the basic OO principles, similar to what you've realized, is that sub-class must obey parent class contracts (including implementing all the methods). Existing code and other parts of the language are based on this assumption and they all needs to be checked if this is changing.

For example, someone creates a sub-type of the TextInput interface and define a new setValue(number) method. For the classes implementing this, it is unclear which version of setValue they should implement. For callers, it is unclear if calling setValue with string is valid or not.

From your examples, what you really want is to generate some compiler messages when the unwanted methods are used and hide them from tools and this is exactly what Java's @Deprecated (or C#'s Obsolete) annotation can do.

someone creates a sub-type of the TextInput interface and define a new setValue(number) method

We could possibly let everything else work as normal:

interface NumberAndTextInput extends TextInput {

    setValue(value: number): void; // Error: Types of property setValue are incompatible

     setValue(value: number|string) : void; // Okay
}

I doubt there is something we want to do here beyond supporting some decorators that might be generally useful (obsolete/deprecated and perhaps one that you can use to hide a member from appearing in Intellisense). I would much rather someone have to occasionally check the documentation to understand members (this particular case feels entirely doable just in doc comments that would surface in signature help, you wouldn't even have to leave VS) than have to constantly check whether types are structurally equivalent.

This issue should be marked as fixed.

type StringLiteralDiff<T extends string, U extends string> = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T];
type Omit<T, K extends keyof T> = Pick<T, StringLiteralDiff<keyof T, K>>;

interface Input<TValue> {

    getValue(): TValue;
    setValue(value: TValue): void;
    clear(): void;
}

interface TextInput extends Omit<Input<string>, 'setValue'> {

    setText(text: string): void;
}

let x = {} as TextInput;
x.setValue('s'); // We get the expected error: Property setValue does not exist on type 'TextInput' 

Although, older and wiser now, I wouldn't recommend anyone actually doing that!

@NoelAbrahams In Context of your code, How can I remove Multiple Properties?

@besrabasant, you can provide a union to Omit.

interface TextInput extends Omit<Input<string>, 'setValue' | 'clear'> {

    setText(text: string): void;
}

let x = {} as TextInput;
x.clear // Required error

@NoelAbrahams thanks. I knew about union types but never thought of that.:blush:

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siddjain picture siddjain  路  3Comments

Roam-Cooper picture Roam-Cooper  路  3Comments

weswigham picture weswigham  路  3Comments

seanzer picture seanzer  路  3Comments

wmaurer picture wmaurer  路  3Comments