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"
Related
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.`);
}
}
};
Somewhere along the way, I added a constructor to my Todo class:
export class Todo {
id: number;
title: string;
complete: boolean = false;
editMode: boolean = false;
constructor(values: Object = {}) {
Object.assign(this, values);
}
}
I don't understand the purpose of the code in the constructor.
My application seems to work both with and without it, but I am hesitant to remove the code
What is the purpose of Object.assign(...) in this constructor?
This a method to easily add the values of the parameters of a class to their respective class fields where a class implements that interface or at least has a partial implantation of that interface.
interface IPerson {
firtName: string;
lastName: string;
}
class Person implements IPerson {
public firtName!: string;
public lastName!: string;
constructor(params: IPerson) {
Object.assign(this, params);
}
}
Your application works because you seem to have implemented this in such a way that the callback value of values to also be enough.
The main issue with this Hack is that Object.assign is not type safe. So using it in this way in a way goes against the point of TypeScript.
If you want to do this in a type safe fashion you are better off using a custom implementation where the type is properly checked. Something like this:
type PDM = PropertyDescriptorMap;
export class ClassSAssign<T> {
constructor(private objectToSpread: T, private klass: T) {}
private propertyDescriptorOptions = {
enumerable: true,
writable: true
};
public apply(): void {
const map = this.getPropertiesDescriptorMap();
Object.defineProperties(this.klass, map);
}
private getPropertiesDescriptorMap(): PDM {
return Object.entries(this.objectToSpread).reduce(
(obj: PDM, entry) => this.getPropertyDescriptorMap(obj, entry),
{}
);
}
private getPropertyDescriptorMap(obj: PDM, [key, value]: [string, any]): PDM {
return {
...obj,
[key]: {
value,
...this.propertyDescriptorOptions
}
};
}
}
and you can use this utility like this:
class Person implements IPerson {
public firtName!: string;
public lastName!: string;
constructor(params: IPerson) {
new ClassSAssign(params, this).apply();
}
}
If you don't/can't want to use the above, I suggest you at least add some type rigour to protect your class from what values can be passed into it
interface IToDo {
id?: number;
title?: string;
}
export class Todo implements IToDo {
public id?: number;
public title?: string;
public complete: boolean = false;
public editMode: boolean = false;
constructor(values?: IToDo) {
Object.assign(this, values);
}
}
Object.assign assigns all of the properties of the second argument to the first argument.
What the code does is if you pass an object into the constructor, it will assign those properties to the object that is being made. So for instance:
const todo = new Todo({ id: 1, title: 'hello' });
console.log(todo.title); // 'hello'
Edit:
Because Object.assign is not type-safe, you should probably have the constructor accept something more specific than just an Object. I would suggest creating an interface for it.
Object.assign has no type checking. An alternative would be:
const assign = <T, K extends keyof T>(...args: T[]): T =>
args.reduce( (result, current) =>
(Object.keys(current) as K[]).reduce((target, key) => {
target[key] = current[key];
return target;
}, result)
, args[0])
;
Note that if T's properties aren't optional, every object passed in must include every property. If you can guarantee the presence of every property after the function returns, you can pass in the arguments as Partial<T>, then coerce the result when you're done.
Its just combining the two objects this and values. According to MDN
The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.
Its used to create a shallow copy of the object and merge its properties with this which is the instance of Todo. In your given code this target object. Consider the below example
let target = {a:1,b:2};
Object.assign(target,{x:"new prop 1",y:"new prop 2"});
console.log(target)
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.
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!
We are using simple function declaration quite a lot where function takes either single object or array of objects of some type.
Simple declaration is:
interface ISomeInterface {
name: string;
}
class SomeClass {
public names: ISomeInterface[] = [];
public addNames(names: ISomeInterface | ISomeInterface[]): void {
names = (!Array.isArray(names)) ? [names] : names;
this.names = this.names.concat(names);
}
}
But TypeScript throws "type is not assignable" error.
Is there better way of doing this? Obviously we could have two separate functions, but I think handling single vs multiple this way is quite fine.
You can make it easier
addNames(names: ISomeInterface | ISomeInterface[]): void {
this.names = this.names.concat(names);
}
From MDN
The concat() method returns a new array comprised of the array on which it is called joined with the array(s) and/or value(s) provided as arguments.
You could also use the rest parameter:
interface ISomeInterface {
name: string;
}
class SomeClass {
public names: ISomeInterface[] = []; // create an instance if applicable.
addNames(...names: ISomeInterface[]): void {
// the names argument will always be an array
this.names = this.names.concat(names);
}
}
You can call it like:
addNames(name1); // just pass one
addNames(name1, name2, name3); // pass more comma separated
addNames(...[name1, name2, name3]); // pass an array.
Please note that I removed the function keyword, because otherwise the this keyword inside the body block might lose scope depending on who's calling it.
I think this is what you want
interface ISomeInterface {
name: string;
}
class SomeClass {
public names: ISomeInterface[];
addNames(names: ISomeInterface | ISomeInterface[]): void {
names = (names instanceof Array) ? names : [names];
this.names = this.names.concat(<ISomeInterface[]>names)
}
}
You want to use instanceOf, not isArray.
The official way typescript handles this is with multiple function signatures, for example:
addNames(names: ISomeInterface): void;
addNames(names: ISomeInterface[]): void;
addNames(names: any): void {
...
}
You can see more information in the official handbook here