Declare interface of class only with methods signature without names - javascript

Suppose I have a class with many methods, but I know for sure that their signature matches.
Is it possible to describe the interface of this class without describing the specific methods of this class in it? Like here:
interface IController {
(input: string): number // any method without reference to its name
}
class Controller implements IController {
method1(input: string): number { ...do something }
method2(input: string): number { ...do something }
...
}
Or is it impossible?

The option to have an index signature (as #fk82 outlines in his answer) has the undesired consequence of forcing you to add an index signature to the class. This means that your class will be indexable by an arbitrary string, which might not be what you want.
If your goal is just to force the implementer class to only have methods with the given signature, a better option is to use a mapped type:
type IController<K extends PropertyKey> = {
[P in K]: (input: string) => number;
}
class Controller implements IController<keyof Controller> {
method1(input: string): number { return input.length; }
method2(input: string): number { return input === '' ? 0 : 1; }
}
let a = new Controller();
a['aa'] // not allowwed under no implicit any
This has the bonus advantage of allowing the class to have some methods that do not conform to the signature if needed, but in an explicit way:
class Controller implements IController<Exclude<keyof Controller, 'special'>> {
method1(input: string): number { return input.length; }
method2(input: string): number { return input === '' ? 0 : 1; }
special() { }
}

You can use an index signature
interface IController {
[name: string]: (input: string) => number;
}
A small caveat is that the TypeScript compiler will now require you to add the index signature to each class which implements IController. I.e. you need to define your Controller class as follows:
class Controller implements IController {
[name: string]: (input: string) => number;
method1(input: string): number { return input.length; }
method2(input: string): number { return input === '' ? 0 : 1; }
}
Here's a TS playground with a full example. Note that the index signature will be tested in assertions such as
const A = {
m(input: string): number { return input.length; },
} as IController;
const B = {
m(input: string): string { return input; }
} as IController;
and the assignment of B will will raise a type error because of the string return value.

You can hack something in order to fit your sought out solution, as per #FK82's answer, but that'd defeat the purpose of the Interface construct, which is to bind an object to a bunch of compile-time known signatures. How would the compiler know what method in particular you are referring to when referencing the Interface?
However, from what I can tell, instead of trying to cram an Interface-based solution, why not declare a functional abstraction in your executing code instead? Just describe the function signature and swap to the proper method as you see suit, since in JS/TS functions are a First-class citizen.
type Strategy = (input: string) => number;
class Controller implements IController {
method1(input: string): number { ...do something }
method2(input: string): number { ...do something }
}
function useMethod(f: Strategy): any {
...
const i = f('my string');
...
}
function main() {
const c = new Controller ();
const method = chooseOne === true ? c.method1 : c.method2;
useMethod (method);
}
This way of doing things is not too disimilar to the Strategy Pattern in OOP, however the FP solution is leaner and boasts what's in my opinion one of the better features of Javascript/Typescript.

Related

How to implement a Typescript interface that allows additional properties?

Ok, I have been struggling with this one as all information I find is about how to define interfaces that allow other properties, but not how to create a class that can implement the interface.
I have (or want to have) the following interface:
export interface IEnvironment {
value: string;
names: string[];
[x: string | 'value' | 'names']: (() => boolean) | string | string[]
};
Then I want a class that implements said interface, but I only want to implement the value and names properties.
For full disclosure, I want to create an environment object with value, names and one function per name in names. Like this:
export class Environment implements IEnvironment {
value: string;
names: Array<string>;
static defaultNames: string[] = ['Development', 'PreProduction', 'Production'];
constructor(value: string, names?: Array<string>) {
this.value = value;
this.names = names ?? Environment.defaultNames;
let currEnvFound = false;
this.names.forEach((name) => {
// Look at all the hoops I had to jump so TypeScript would not complain. Suggestions welcome.
(this as unknown as { [x: string]: () => boolean })[`is${name}`] = function () { return (this as unknown as Environment).value === name; };
currEnvFound = currEnvFound || name === value;
});
// Throw if the current environment value was not found.
if (!currEnvFound) {
throw new Error(`The provided environment value "${value}" was not found among the provided list of environments.`);
}
}
};
Now this works except for one error I get:
Class 'Environment' incorrectly implements interface 'IEnvironment'.
Index signature for type 'string' is missing in type 'Environment'.
So how can I do this? I'm a noob in the TypeScript arena, so I'd rather ask the experts.
If no solution, could this be worked around with another interface that extends IEnvironment? Like remove the extra properties thing and move it to another interface that I would use as consumer of the object so I get the correct Intellisense.
Thank you very much in advance.
You just need to declare this dynamic x property from interface as class property.
Add this line as your class property: [x: string]: string|(() => boolean)|string[];
Finally, your class looks like this:
class Environment implements IEnvironment {
value: string;
names: Array<string>;
static defaultNames: string[] = ['Development', 'PreProduction', 'Production'];
//ADD THIS
[x: string]: string|(() => boolean)|string[];
constructor(value: string, names?: Array<string>) {
this.value = value;
this.names = names ?? Environment.defaultNames;
let currEnvFound = false;
this.names.forEach((name) => {
// Look at all the hoops I had to jump so TypeScript would not complain. Suggestions welcome.
(this as unknown as { [x: string]: () => boolean })[`is${name}`] = function () { return (this as unknown as Environment).value === name; };
currEnvFound = currEnvFound || name === value;
});
// Throw if the current environment value was not found.
if (!currEnvFound) {
throw new Error(`The provided environment value "${value}" was not found among the provided list of environments.`);
}
}
};

TypeScript: define dynamic properties in proxy

I have the following call to a watch
const watch = hp.watch({
running: false,
time: 0,
start: Date.now()
})
watch bassically just runs new proxy(), then sets some properties and returns the newly created proxy class nothing too fancy.
export function watch(item: { [key: string]: any }): proxy
export function watch(key: string, value: any): proxy
export function watch(...args: any[]): proxy {
let prox = new proxy()
if (args.length == 2) {
prox[args[0]] = args[1]
} else if (args.length == 1 && args[0] instanceof Object) {
for (let itm in args[0]) {
!(itm in prox) && (prox[itm] = args[0][itm])
}
}
return prox
}
I then have an interface which looks like this:
export interface proxy {
[key: string]: any
}
Here is the the proxy class which basically is just a wrapper.
namespace hp {
export class proxy {
public constructor() {
return new Proxy(this, { /* Proxy stuff */})
}
}
}
In an editor that supports intellisense, it would be nice if I could have it suggest running, time, start after I type watch..
I think I need to use a more advanced interface (or type) than the one I am using for that to happen. I have tried this but it doesn't work:
export type watch<T> = {
[A in keyof T]: T[A]
}
export interface proxy {
[key: string]: watch<any>
}
When doing watch.time = 123 I get an error stating:
Type 'number' is not assignable to type 'watch'.
and when trying to get the value let a = watch.time I get this error:
The right-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type.
You want to change the signature for hp.watch() to something like
export function watch<T>(item: T): proxy & T;
export function watch<K extends string, V>(key: K, value: V): proxy & Record<K, V>;
export function watch(...args: any[]): proxy {
// impl
}
Then you have told TypeScript that the output of the function is both a proxy and has the same keys and value types as the thing you passed in.
Hope that helps; good luck!

Error: Cannot invoke an expression whose type lacks a call signature

I am brand new to typescript, and I have two classes. In the parent class I have:
abstract class Component {
public deps: any = {};
public props: any = {};
public setProp(prop: string): any {
return <T>(val: T): T => {
this.props[prop] = val;
return val;
};
}
}
In the child class I have:
class Post extends Component {
public toggleBody: string;
constructor() {
this.toggleBody = this.setProp('showFullBody');
}
public showMore(): boolean {
return this.toggleBody(true);
}
public showLess(): boolean {
return this.toggleBody(false);
}
}
Both showMore and ShowLess give me the error, "Cannot invoke an expression whose type lacks a call signature."
But the function that setProp returns DOES have a call signature, I think? I think I'm misunderstanding something important about typings of functions, but I don't know what it is.
Thanks!
The function that it returns has a call signature, but you told Typescript to completely ignore that by adding : any in its signature.
"Cannot invoke an expression whose type lacks a call signature."
In your code :
class Post extends Component {
public toggleBody: string;
constructor() {
this.toggleBody = this.setProp('showFullBody');
}
public showMore(): boolean {
return this.toggleBody(true);
}
public showLess(): boolean {
return this.toggleBody(false);
}
}
You have public toggleBody: string;. You cannot call a string as a function. Hence errors on : this.toggleBody(true); and this.toggleBody(false);
Let's break this down:
The error says
Cannot invoke an expression whose type lacks a call signature.
The code:
The problem is in this line public toggleBody: string; &
it's relation to these lines:
...
return this.toggleBody(true);
...
return this.toggleBody(false);
The result:
Your saying toggleBody is a string but then your treating it like something that has a call signature (i.e. the structure of something that can be called: lambdas, proc, functions, methods, etc. In JS just function tho.). You need to change the declaration to be public toggleBody: (arg: boolean) => boolean;.
Extra Details:
"invoke" means your calling or applying a function.
"an expression" in Javascript is basically something that produces a value, so this.toggleBody() counts as an expression.
"type" is declared on this line public toggleBody: string
"lacks a call signature" this is because your trying to call something this.toggleBody() that doesn't have signature(i.e. the structure of something that can be called: lambdas, proc, functions, methods, etc.) that can be called. You said this.toggleBody is something that acts like a string.
In other words the error is saying
Cannot call an expression (this.toggleBody) because it's type (:string) lacks a call signature (bc it has a string signature.)
It means you're trying to call something that isn't a function
const foo = 'string'
foo() // error
I think what you want is:
abstract class Component {
public deps: any = {};
public props: any = {};
public makePropSetter<T>(prop: string): (val: T) => T {
return function(val) {
this.props[prop] = val
return val
}
}
}
class Post extends Component {
public toggleBody: (val: boolean) => boolean;
constructor () {
super()
this.toggleBody = this.makePropSetter<boolean>('showFullBody')
}
showMore (): boolean {
return this.toggleBody(true)
}
showLess (): boolean {
return this.toggleBody(false)
}
}
The important change is in setProp (i.e., makePropSetter in the new code). What you're really doing there is to say: this is a function, which provided with a property name, will return a function which allows you to change that property.
The <T> on makePropSetter allows you to lock that function in to a specific type. The <boolean> in the subclass's constructor is actually optional. Since you're assigning to toggleBody, and that already has the type fully specified, the TS compiler will be able to work it out on its own.
Then, in your subclass, you call that function, and the return type is now properly understood to be a function with a specific signature. Naturally, you'll need to have toggleBody respect that same signature.
This error can be caused when you are requesting a value from something and you put parenthesis at the end, as if it is a function call, yet the value is correctly retrieved without ending parenthesis. For example, if what you are accessing is a Property 'get' in Typescript.
private IMadeAMistakeHere(): void {
let mynumber = this.SuperCoolNumber();
}
private IDidItCorrectly(): void {
let mynumber = this.SuperCoolNumber;
}
private get SuperCoolNumber(): number {
let response = 42;
return response;
};
Add a type to your variable and then return.
Eg:
const myVariable : string [] = ['hello', 'there'];
const result = myVaraible.map(x=> {
return
{
x.id
}
});
=> Important part is adding the string[] type etc:
I had the same error message. In my case I had inadvertently mixed the ES6 export default function myFunc syntax with const myFunc = require('./myFunc');.
Using module.exports = myFunc; instead solved the issue.

Typescript index get/set

I am implementing the Typescript Array interface. I wonder if there is any possibility to define the get/set for indexes. For example:
class MyClass<T> implements Array<T> {
[index: number] : T;
// ... other methods
}
Is there a possibility to write in a following way:
class MyClass<T> implements Array<T> {
get [index: number] () : T {
// implementation
}
set [index: number] (value: T) : void {
// implementation
}
// ... other methods
}
No, overloading the index operator cannot be done on classes; however, you can define this with an ES6 Proxy, but many browsers don't support that yet.
One alternative is to create a wrapper around an array in order to force people to use the methods, where you can put additional functionality:
class MyContainer<T> {
private items: T[] = [];
get(name: string): T {
// some additional functionality may go here
return this.items[name];
}
set(name: string, value: T) : void {
// some additional functionality may go here
this.items[name] = value;
// or here
}
}
const myContainer = new MyContainer<string>();
myContainer.set("key", "value");
console.log(myContainer.get("key")); // "value"

Implement ng.IFilterService in Typescript

I have the following in a .ts file
module App.Filters {
export class SplitRangeFilter implements ng.IFilterService {
static $inject = ['$filter'];
public static factory(): Function {
return (input: string, splitIndex: number) => {
return input.split('-')[splitIndex];
}
}
}
angular.module("App.Filters", []).filter('SplitRange', () => SplitRangeFilter.factory);
}
and it is giving me a compiler error of:
Class SplitRangeFilter declared interface IFilterService but does not implement it: Types 'SplitRangeFilter' and 'IFilterService' have incompatible signatures. No matching signature for '<T>(name: string) => T'
I cannot find any such signature in angulars documentation. Any suggestions on how to get rid of this compiler error in VS2015
The IFilterService is only used to add type safety via TypeScript compiler to programmatic filter invokes.
The correct implementation based on your code would be:
module App.Filters {
// Add filter type to the filter service so that TypeScript can
// perform type checking on future programmatic invokes.
// This app filter service
export interface IAppFilterService extends angular.IFilterService {
(name: 'SplitRange'): (input: string | splitIndex: number) => string | undefined;
}
export class SplitRangeFilter {
constructor() {
return (input: string, splitIndex: number): string | undefined => {
return input.split('-')[splitIndex];
}
}
}
angular.module("App.Filters", []).filter('SplitRange', SplitRangeFilter);
}
This is how IAppFilterService will be used in your code:
// Usage for IAppFilterService
class AppController {
constructor(private $filter: IAppFilterService) {}
// This will pass without compile warnings
public splitString(val: string, index: number): string | undefined {
return this.$filter('SplitRange')(val, number);
}
// This will throw compile time TypeScript error
public splitNumber(val: number, index: number): string | undefined {
return this.$filter('SplitRange')(val, number);
}
}
If you just want it to work and aren't concerned about implementing the "right" interface then remove the implements ng.IFilterService and then change your Angular registration statement to execute the factory (i.e. add ())
angular.module("App.Filters", []).filter('SplitRange', () => SplitRangeFilter.factory());

Categories

Resources