I'm in the process of optimizing some code in my library, however, I have a bit of an issue regarding why bracket notation isn't working when trying to call an imported class.
Parameter type accepts a string that is camelCased, such as: myString.
The parameter data can be anything.
import { foo } from './example';
export const find = (type: string, data: any) => {
// This next line effectively deletes the end of the string starting
// from the first capital letter.
const f = type.replace(/[A-Z][a-z]+/, '');
try {
return [f][type](data);
} catch (e) {
return e;
}
};
this is what I expect it to look like if I was to visualize it using dot notation:
foo.fooBar(someRandomData)
This should call the static method fooBar(data) on the imported class foo,
however, I receive an error message:
TypeError: [f][type] is not a function
If I was to revert it back to my if..else if style, it works:
if (type.startsWith('foo')) return foo[type](data);
How can I do what is desired above without getting the defined error message?
Thank you for your help in advance!
EDIT: This is an example I modified from already existing code, therefore, I fixed a few typos.
EDIT #2: as per requested, the imported class foo looks like this:
export class foo{
static fooBar(data){
// Do stuff with data...
}
In the end you need some reference to the classes or object to get started with. Here is a working example of how you could do this type of functionality, but you have start with a map of your class instances so you can get to them:
class foo {
fooBar(data: any) { return { name: 'foo', data } };
}
class example {
exampleFood(data: any) { return { name: 'example', data } };
}
var lookup: { [classes: string]: any; } = { };
lookup['foo'] = new foo();
lookup['example'] = new example();
const find = (encodedType: string, data: any) => {
// This next line effectively deletes the end of the string starting
// from the first capital letter.
const f = encodedType.replace(/[A-Z][a-z]+/, '');
try {
return lookup[f][encodedType](data);
} catch (e) {
return e;
}
};
alert(JSON.stringify(find("fooBar", "Found you foo")));
alert(JSON.stringify(find("exampleFood", "Found you example")));
I would suggest you instead move over to using the nodeJS built-in EventEmitter.
You can do something like:
import * as EventEmitter from 'events';
import { foo } from './example';
import { bar } from './example2';
export const discordEventEmitter = new EventEmitter();
discordEventEmitter.on('fooBar', foo.fooBar);
discordEventEmitter.on('fooToo', foo.fooToo);
discordEventEmitter.on('barBell', bar.barBell);
Then, when you want to fire an event, you can simply:
discordEventEmitter.emit('fooBar', someData);
You can also simplify the event handler registration by writing:
const fooProps = Object.getOwnPropertyNames(foo) as (keyof typeof foo)[];
fooProps.filter(k => typeof foo[k] === 'function').forEach(funcName => {
discordEventEmitter.on(funcName, foo[funcName]);
});
const barProps = Object.getOwnPropertyNames(bar) as (keyof typeof bar)[];
fooProps.filter(k => typeof bar[k] === 'function').forEach(funcName => {
discordEventEmitter.on(funcName, bar[funcName]);
});
Related
In the 3rd party lib, There's the following:
export interface Event extends Log {
args?: Result;
}
export interface TypedEvent<EventArgs extends Result> extends Event {
args: EventArgs;
}
export type InstallationPreparedEvent = TypedEvent<
[
string,
string
] & {
sender: string;
permissions : string;
}
>;
Then, in my code, I got this.
function prepareInstall(...): Promise<InstallationPreparedEvent> {
const event = ...;
return event;
}
Then, I got these choices, but I like none of them. The reason is, I simplified it, but it actualy contains 8 args, so code becomes ugly.
// choice 1.
function run() {
const event = await prepareInstall();
string sender = event.args.sender;
}
// choice 2
function run() {
const { event : {sender } } = await prepareInstall();
}
// choice 3
function run() {
const { sender } = (await prepareInstall()).args;
}
What I want to achieve is from prepareInstall, I want to return event.args directly so I'd use it like this:
const { sender } = await prepareInstall();
But returning event.args doesn't solve it as typescript in the run still requires me to do: event.args.sender. The important thing is I need returned type in prepareInstall to be InstallationPreparedEvent since in run, I want typescript to tell me what values exist(sender, permissions).
Hope I made sense.
If I understand correctly, you want to return only the arg prop from InstallationPreparedEvent, but with correct typing (right?).
You can specify a prop's type through bracket notation:
function prepareInstall(...): Promise<InstallationPreparedEvent["args"]> {
const event = await ...;
return event.args;
}
With this, you will get type hinting:
function run() {
const eventArgs = await prepareInstall();
const {sender} = eventArgs; // no complaints
const foo = eventArgs.foo; //Error: Property 'foo' does not exist on type '[string, string] & { sender: string; permissions: string; }'
}
This is a bit of a mock-up: I have test class and columnHeader class. The test class calls a function with a loop through all the table columns on the table. For each column, I would like to apply either filtering or a sorting.
With the following code my problem is, that eslint complains:
let columnHeader: ColumnHeader Avoid referencing unbound methods which
may cause unintentional scoping of this.
and even more the variable tableNam is not defined in the callback functions (clickColumnActionFilter,clickColumnSort).
The test file:
import { ColumnHeader } from '../../../support/spa/ColumnHeader';
export function tableColumnActionTest() {
describe(`Table column sorting and filtering`, () => {
let columnHeader = new ColumnHeader();
before(() => {
A_page.visit(//some url);
columnHeader = new ColumnHeader();
columnHeader.setTableN('aTable');
});
it.only('Select column action tests', () => {
columnHeader.loopThroughColumnHeader(columnHeader.clickColumnActionFilter);
});
});
}
The class file:
import { table } from './Table';
interface Action {
(colId: string): void;
}
export class ColumnHeader {
tableNam: string;
setTableN(tabN: string) {
this.tableNam = tabN;
}
clickColumnSort(colName: string) {
cy.log(`tableName: ${this.tableNam}`);
// do sorting
}
clickColumnActionFilter(colName: string) {
cy.log(`tableName: ${this.tableNam}`);
// do filtering
}
loopThroughColumnHeader(columnAction: Action): void {
for (let i = 1; i <= 10; i++) {
table.getColumnIdFromNr(i);
cy.get('#COLID').then(($colIDent) => {
const colId = $colIDent as unknown as string;
columnAction(colId);
});
cy.wait(500);
}
});
table.getColumnNameFromNr(2);
}
}
export const columnHeader = new ColumnHeader();
Do you know how to call the function name as argument properly so that the environment is correct?
Ok I found out:
In the test file, I needed to bind the columnheader.
This solved the eslint complaint and the undefined member variable
columnHeader.loopThroughColumnHeader(columnHeader.clickColumnActionFilter.bind(columnHeader));
instead of just:
columnHeader.loopThroughColumnHeader(columnHeader.clickColumnActionFilter)
Consider this:
interface TArguments {
width?: number,
height?: number
}
interface TSomeFunction {
someFunction ({width, height}: TArguments): void
}
const someObject: TSomeFunction = {
someFunction ({width, height}) {
// do something, no return
}
}
Both paremeters are optional, this means that I can call someFunction like that:
someObject.someFunction() // but it is not passing through
I'm geting an Error "Expected 1 arguments, but got 0".
Am I missing something ?
How do I write an Interface when all parameters are optional?
Your interface should not concern about default values or destructuring, as those are implementation details. Just declare the parameter as optional, as if it was a scalar:
interface TSomeFunction {
someFunction (size? : TArguments): void
}
The implementation can then define both:
const someObject: TSomeFunction = {
someFunction({ width, height } = {}) {
// do something, no return
}
}
Your error must be coming from a different place (or try restarting your IDE, that is a source of a lot of frustration :)). Your second approach looks correct:
const someFunction = ({width, height}: TArguments = {}) => { ... }
Here is a working TypeScript playground link
EDIT You need to also specify that the parameter itself, not only its keys, is optional:
interface TSomeFunction {
someFunction (arguments?: TArguments): void; // Notice the question mark after the parameter expression
}
// Now you can either add a default value to your function
const someObject: TSomeFunction = {
someFunction ({width, height}: TArguments = {}) {}
}
// Or leave it optional without specifying a default value
const someObject: TSomeFunction = {
someFunction ({width, height}?: TArguments) {} // Notice the question mark
}
Here's my code to test equality of some class objects. See my other question if you want to know why I'm not just doing
expect(receivedDeals).toEqual(expectedDeals) and other simpler assertions.
type DealCollection = { [key: number]: Deal }; // imported from another file
it("does the whole saga thing", async () => {
sagaStore.dispatch(startAction);
await sagaStore.waitFor(successAction.type);
const calledActionTypes: string[] = sagaStore
.getCalledActions()
.map(a => a.type);
expect(calledActionTypes).toEqual([startAction.type, successAction.type]);
const receivedDeals: DealCollection = sagaStore.getLatestCalledAction()
.deals;
Object.keys(receivedDeals).forEach((k: string) => {
const id = Number(k);
const deal = receivedDeals[id];
const expected: Deal = expectedDeals[id];
for (let key in expected) {
if (typeof expected[key] === "function") continue;
expect(expected[key]).toEqual(deal[key]);
}
});
});
The test passes fine, but I'm getting a Flow error on expected[key]:
Cannot get 'expected[key]' because an index signature declaring the expected key / value type is missing in 'Deal'
I can paste in code from Deal by request, but I think all you need to know is that I haven't declared an index signature (because I don't know how!).
I've searched around a bit but I can't find this exact case.
Update: I can eliminate the errors by changing deal and expected thusly:
const deal: Object = { ...receivedDeals[id] };
const expected: Object = { ...expectedDeals[id] };
And since I'm comparing properties in the loop this isn't really a problem. But I would think that I should be able to do this with Deals, and I'd like to know how I declare the index signature mentioned in the error.
PS. Bonus question: In some world where a mad scientist crossbred JS with Swift, I imagine you could do something like
const deal: Object = { ...receivedDeals[id] where (typeof receivedDeals[id] !== "function" };
const expected = // same thing
expect(deal).toEqual(expected);
// And then after some recombining of objects:
expect(receivedDeals).toEqual(expectedDeals);
Is this a thing at all?
Edit:
Adding a bit of the definition of Deal class:
Deal.js (summary)
export default class Deal {
obj: { [key: mixed]: mixed };
id: number;
name: string;
slug: string;
permalink: string;
headline: string;
// ...other property definitions
constructor(obj?: Object) {
if (!obj) return;
this.id = obj.id;
this.name = obj.name;
this.headline = obj.headline;
// ...etc
}
static fromApi(obj: Object): Deal {
const deal = new Deal();
deal.id = obj.id;
deal.name = obj.name;
deal.slug = obj.slug;
deal.permalink = obj.permalink;
// ...etc
return deal;
}
descriptionWithTextSize(size: number): string {
return this.descriptionWithStyle(`font-size:${size}`);
}
descriptionWithStyle(style: string): string {
return `<div style="${style}">${this.description}</div>`;
}
distanceFromLocation = (
location: Location,
unit: unitOfDistance = "mi"
): number => {
return distanceBetween(this.location, location);
};
distanceFrom = (otherDeal: Deal, unit: unitOfDistance = "mi"): number => {
return distanceBetween(this.location, otherDeal.location);
};
static toApi(deal: Deal): Object {
return { ...deal };
}
static collectionFromArray(array: Object[]) {
const deals: DealCollection = {};
array.forEach(p => (deals[p.id] = Deal.fromApi(p)));
return deals;
}
}
An index signature (or indexer property) is defined as [keyName: KeyType]: ValueType. DealCollection is a great example: the keyName is key, the KeyType is number and the ValueType is Deal. This means that whenever you access a number property of an object of type DealCollection, it will return a Deal. You will want to add a similar expression to the definition of Deal in order to access arbitrary properties on it. More information can be found at the Objects as maps section in the Flow documentation.
I've got this EventsStorage typescript class that is responsible for storing and retrieving Event objects in ionic-storage (wrapper for sqlite and indexedDB). It uses my Event class throughout.
I would like to reuse a lot of this logic for something other than an Event, like a Widget.
I come from a ruby background where it would be relatively simple to extract all the storage logic, set a ruby var that is literally the class Event and use that var wherever I use Event. Can I do something similar in typescript? Is there another mechanic I can use to reuse the bulk of this class for something else, like Widget?
Ideally, my EventsStorage class becomes really lightweight, and I'm not just wrapping calls to this.some_storage_module.get_ids() or this.some_storage_module.insert_new_objs() -- which would have to be copy/pasted to every other instance I needed this.
Something like this:
export class EventsStorage { // extends BaseStorage (maybe??)
constructor(){
super(Event, 'events'); // or some small set of magical args
}
}
Here's the existing class:
import { Injectable } from '#angular/core';
import { Storage } from '#ionic/storage';
import { Event } from '../classes/event';
// EventsStorage < EntityStorage
// - tracks local storage info
// - a key to an array of saved objects
// - a query() method that returns saved objects
#Injectable()
export class EventsStorage {
base_key: string;
ids_key: string;
constructor(
private storage: Storage
){
this.base_key = 'event';
this.ids_key = [this.base_key, 'ids'].join('_');
}
get_ids(): Promise<any>{
return this.storage.ready().then(() => {
return this.storage.get(this.ids_key).then((val) => {
if(val === null){
return [];
} else {
return val;
}
});
});
}
insert_new_objs(new_objs: any): Promise<any>{
return new_objs.reduce((prev: Promise<string>, cur: any): Promise<any> => {
return prev.then(() => {
return this.storage.set(cur._id, cur.event);
});
}, Promise.resolve()).then(() => {
console.log('saving event_ids');
return this.storage.set(this.ids_key, new_objs.map(obj => obj._id));
});
}
update(events: Event[]): Promise<any> {
let new_objs = events.map((event) => {
return {
_id: [this.base_key, event.id].join('_'),
event: event
};
});
return this.insert_new_objs(new_objs);
}
query(): Promise<Event[]>{
let events = [];
return this.get_ids().then((ids) => {
return ids.reduce((prev: Promise<string>, cur: string): Promise<any> => {
return prev.then(() => {
return this.get_id(cur).then((raw_event) => {
events = events.concat([raw_event as Event]);
return events;
});
});
}, Promise.resolve());
});
}
get_id(id: string): Promise<Event>{
return this.storage.get(id).then((raw_event) => {
return raw_event;
});
}
}
It looks to me like you want to use generics. You basically define some basic interface between all the things you'll want to store, and your code should depend on that interface. In your code as far as I can tell you only use the id property.
So it would look kinda like this
import { Event } from '...';
import { Widget } from '...';
interface HasId{
id: string;
}
class ItemsStorage<T extends HasId> {
....
get_id(id: string): Promise<T>{
...
}
}
const EventStorage = new ItemsStorage<Events>(storage);
const WidgetStorage = new ItemsStorage<Widget>(storage);
const ev = EventStorage.get_id('abc'); //type is Promise<Event>
const wd = WidgetStorage.get_id('def'); //type is Promise<Widget>
You can read more about generics here.
Edit:
1 - about subclassing - It's usually less preferable. If your ItemsStorage class need different behavior when dealing with Events vs Widgets, than subclassing is your solution. But if you have the same behavior for every class, one might call your code generic, and using generics is better.