Turning map into plain object for TS declaration - node.js

I have this:
let cachedPromises: Map<string, Promise<any>> = new Map();
what is the equivalent declaration for a plain object?
Something like this:
interface IMyMap {
[key: string]: Promise<any>
}
let cachedPromises: IMyMap = {};
is that sufficient?

This is sufficient, but it does come as a double edged sword in that you can't implement the interface on a class that has any property that does not return a Promise:
class Person implements IMyMap {
[key: string]: Promise<any>
constructor(private firstName) { // Error: Property 'firstName' of type 'string' is not assignable to string index type 'Promise<any>'.
}
}

Related

TypeScript - Repository pattern with Sequelize

I'm converting my Express API Template to TypeScript and I'm having some issues with the repositories.
With JavaScript, I would do something like this:
export default class BaseRepository {
async all() {
return this.model.findAll();
}
// other common methods
}
import BaseRepository from './BaseRepository';
import { User } from '../Models';
export default class UserRepository extends BaseRepository {
constructor() {
super();
this.model = User;
}
async findByEmail(email) {
return this.model.findOne({
where: {
email,
},
});
}
// other methods
Now, with TypeScript, the problem is that it doesn't know the type of this.model, and I can't pass a concrete model to BaseRepository, because, well, it is an abstraction. I've found that sequelize-typescript exports a ModelCtor which declares all the static model methods like findAll, create, etc., and I also could use another sequelize-typescript export which is Model to properly annotate the return type.
So, I ended up doing this:
import { Model, ModelCtor } from 'sequelize-typescript';
export default abstract class BaseRepository {
protected model: ModelCtor;
constructor(model: ModelCtor) {
this.model = model;
}
public async all(): Promise<Model[]> {
return this.model.findAll();
}
// other common methods
}
import { Model } from 'sequelize-typescript';
import BaseRepository from './BaseRepository';
import { User } from '../Models';
export default class UserRepository extends BaseRepository {
constructor() {
super(User);
}
public async findByEmail(email: string): Promise<Model | null> {
return this.model.findOne({
where: {
email,
},
});
}
// other methods
}
Ok, this works, TypeScript doesn't complain about methods like findOne or create not existing, but that generates another problem.
Now, for example, whenever I get a User from the repository, if I try to access one of its properties, like user.email, TypeScript will complain that this property does not exist. Of course, because the type Model does not know about the specifics of each model.
Ok, it's treason generics then.
Now BaseRepository uses a generic Model type which the methods also use:
export default abstract class BaseRepository<Model> {
public async all(): Promise<Model[]> {
return Model.findAll();
}
// other common methods
}
And the concrete classes pass the appropriate model to the generic type:
import BaseRepository from './BaseRepository';
import { User } from '../Models';
export default class UserRepository extends BaseRepository<User> {
public async findByEmail(email: string): Promise<User | null> {
return User.findOne({
where: {
email,
},
});
}
// other methods
}
Now IntelliSense lights up correctly, it shows both abstract and concrete classes methods and the model properties (e.g. user.email).
But, as you have imagined, that leads to more problems.
Inside BaseRepository, where the methods use the Model generic type, TypeScript complains that 'Model' only refers to a type, but is being used as a value here. Not only that, but TypeScript also doesn't know (again) that the static methods from the model exist, like findAll, create, etc.
Another problem is that in both abstract and concrete classes, as the methods don't use this anymore, ESLint expects the methods to be static: Expected 'this' to be used by class async method 'all'. Ok, I can just ignore this rule in the whole file and the error is gone. It would be even nicer to have all the methods set to static, so I don't have to instantiate the repository, but maybe I'm dreaming too much.
Worth mentioning that although I can just silence those errors with // #ts-ignore, when I execute this, it doesn't work: TypeError: Cannot read property 'create' of undefined\n at UserRepository.<anonymous>
I researched a lot, tried to make all methods static, but static methods can't reference the generic type (because it is considered an instance property), tried some workarounds, tried to pass the concrete model in the constructor of BaseRepository along with the class using the generic type, but nothing seems to work so far.
In case you want to check the code: https://github.com/andresilva-cc/express-api-template/tree/main/src/App/Repositories
EDIT:
Found this: Sequelize-Typescript typeof model
Ok, I removed some unnecessary code from that post and that kinda works:
import { Model } from 'sequelize-typescript';
export default abstract class BaseRepository<M extends Model> {
constructor(protected model: typeof Model) {}
public async all(attributes?: string[]): Promise<M[]> {
// Type 'Model<{}, {}>[]' is not assignable to type 'M[]'.
// Type 'Model<{}, {}>' is not assignable to type 'M'.
// 'Model<{}, {}>' is assignable to the constraint of type 'M', but 'M' could be instantiated with a different subtype of constraint 'Model<any, any>'.
return this.model.findAll({
attributes,
});
}
import BaseRepository from './BaseRepository';
import { User } from '../Models';
export default class UserRepository extends BaseRepository<User> {
constructor() {
super(User);
}
}
I mean, if I put some // #ts-ignore it at least executes, and IntelliSense lights up perfectly, but TypeScript complains.
We faced the same problem. The solution was to declare returning types with an interface that an abstract repository class implements.
Code for the interface:
export type RepoResult<M> = Promise<Result<M | undefined, RepoError | undefined>>;
export interface IRepo<M> {
save(model: M): RepoResult<M>;
findById(id: string): RepoResult<M>;
search(parameterName: string, parameterValue: string, sortBy: string, order: number, pageSize: number, pageNumber: number): RepoResult<M[]>;
getAll(): RepoResult<M[]>;
deleteById(id: string): RepoResult<M>;
findByIds(ids: string[]): RepoResult<M[]>;
deleteByIds(ids: string[]): RepoResult<any>;
};
Code for the abstract class:
export abstract class Repo<M extends sequelize.Model> implements IRepo<M> {
protected Model!: sequelize.ModelCtor<M>;
constructor(Model: sequelize.ModelCtor<M>) {
this.Model = Model;
}
public async save(doc: M) {
try {
const savedDoc = await doc.save();
return Result.ok(savedDoc);
} catch (ex: any) {
logger.error(ex);
return Result.fail(new RepoError(ex.message, 500));
}
}
public async findById(id: string) {
try {
const doc = await this.Model.findOne({where: {
id: id
}});
if (!doc) {
return Result.fail(new RepoError('Not found', 404));
}
return Result.ok(doc);
} catch (ex: any) {
return Result.fail(new RepoError(ex.message, 500));
}
}
}
Hope it helps. Have a nice day:)
EDIT:
Result is a class that looks like this:
export class Result<V, E> {
public isSuccess: boolean;
public isFailure: boolean;
private error: E;
private value: V;
private constructor(isSuccess: boolean, value: V, error: E) {
if (isSuccess && error) {
throw new Error('Successful result must not contain an error');
} else if (!isSuccess && value) {
throw new Error('Unsuccessful error must not contain a value');
}
this.isSuccess = isSuccess;
this.isFailure = !isSuccess;
this.value = value;
this.error = error;
}
public static ok<V>(value: V): Result<V, undefined> {
return new Result(true, value, undefined);
}
public static fail<E>(error: E): Result<undefined, E> {
return new Result(false, undefined, error);
}
public getError(): E {
if (this.isSuccess) {
throw new Error('Successful result does not contain an error');
}
return this.error;
}
public getValue(): V {
if (this.isFailure) {
throw new Error('Unsuccessful result does not contain a value');
}
return this.value;
}
}
RepoError class:
type RepoErrorCode = 404 | 500;
export class RepoError extends Error {
public code: RepoErrorCode;
constructor(message: string, code: RepoErrorCode) {
super(message);
this.code = code;
}
}
RepoResult type:
export type RepoResult<M> = Promise<Result<M | undefined, RepoError | undefined>>;
You can find more info on the pattern at the link below:
https://khalilstemmler.com/articles/enterprise-typescript-nodejs/functional-error-handling/

Typescript adding types to EventEmitter

I am trying to make my code more readable and concise.
The biggest problem I am currently having is with the EventEmitter class. Every time I create a new class that uses EventEmitter I have to declare all the functions of the EventEmitter to make the class easier to understand and use.
Ex:
interface Hello {
addListener(event: 'hi', listener: (message: string) => void): this;
on(event: 'hi', listener: (message: string) => void): this;
...
}
class Hello extends EventEmitter {
constructor() { super(); }
}
I have searched around for a solution but I couldn't find anything that suits me so I tryed to come up with my own.
interface EventEmitterEvents {
[event: string]: any[];
}
interface EventEmitterType<T extends EventEmitterEvents> extends EventEmitter {
addListener<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this;
on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this;
...
}
class Hello extends EventEmitter implements EventEmitterType<{'hi': [string]}> { ... }
But when I try to implement the interface EventEmitterType it throws an error
types of property 'addListener' are incompatible
I have figured out that for some reason in the 'addListener' and functions alike, type event is said to be 'string' | 'symbol' | 'number' which is incompatible with the EventEmitter where it is 'string' | 'symbol' but in the EventEmitterEvents I have defined that event is of type 'string'.
Question:
Is there any way to fix this, and if not, is there any other way of recreating this functionality (not having to type all those functions)?
Edit:
If I set event argument to 'string' it will still throw error because of listener which is also incompatible with addListener saying 'any[]' is not assignable to type 'T[K]'.
I really like the functionality that you can get from getting this sort of extension right. Anyhow, here are some ideas I would like to share that might help:
Depending on you intention here, maybe not implementing the interface is a good idea, but defining an abstract class that will extend the event emitter only, like so:
interface EventEmitterEvents {
[event: string]: any[];
}
abstract class EventEmitterType<T extends EventEmitterEvents> extends EventEmitter {
protected constructor() {
super();
// do stuff here
}
addListener<K extends keyof T | symbol>(event: K, listener: (...args: T[Extract<string, K>]) => void) {
// do stuff here
return super.addListener(event, listener);
}
on<K extends keyof T | symbol>(event: K, listener: (...args: T[Extract<string, K>]) => void) {
// do stuff here
return super.on(event, listener);
}
}
class Hello extends EventEmitterType<{ hi: [string] }> {}
Bootstrapping the event emitter with the functionality that you
want instead of extending the class like this.
/*
* So the idea is to define a function that will convert the event emitter to your
* desired design by strapping on the new stuff and ideas that you have.
*/
// So we have our interface from your sample code
interface EventEmitterEvents {
[event: string]: any[];
}
interface EventEmitterType<T extends EventEmitterEvents> {
addListener<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this;
on<K extends keyof T>(event: K, listener: (...args: T[K]) => void): this;
...
}
// then you define a function that will cast your interface onto the event emitter
function bootstrap<T extends EventEmitterEvents>(emitter: EventEmitter) {
return emitter as EventEmitterType<T>
}
// this introduces a grey area in your code that you might not like though.
I hope this is of some use to your goal here.
Maybe narrowing the type of event names to string like Extract<keyof T, string> could help.
Or if you just want the result of keyof operator to be a string, add configuration with "keyofStringsOnly": true in tsconfig.json.

typescript class constructor infer type from constructor arguments

Let's say I have a class like this one :
class Entity {
id?: string;
name?: string;
constructor({ id, name }: { id?: string; name?: string }) {
this.id = id;
this.name = name;
}
}
And then I have a function like this which requires an argument of type Entity but with the id property field set:
function myFunc(entity: Entity & { id: string }) {
console.log("do something", entity.id);
}
I would expect the following to work, but it doesn't:
const myEntityWithoutID = new Entity({ name: "no id" });
const myEntityWithID = new Entity({ name: "dummy", id: "dummy" });
myFunc(myEntityWithoutID); // error, as expected
myFunc(myEntityWithID); // error, but this should work
I'm not sure how I can force typescript to correctly infer the type of the class instance. I hope I've given enough information in order to present the problem, does anyone have any idea ?

How to force the type of a variable in typescript?

I'm trying to write some function that converts a mongoose User model to a string with bullet points:
// Simplified version so you get the idea
interface IUser {
name: string
}
function userDetails (user: IUser, keys: string[]): string {
return keys.map((k: string): string => {
return `- ${k} : ${user[k]}`
})
.join('\n')
}
But I'm having a strange compiler error, where user[k] is underlined:
Index signature of object type implicitly has an 'any' type.
Is there a way to "force" typescript to admin that user[k] is a string ?
I tried user[k] as string or <string> user[k] without success.
Also, if I remove the ${user[k]} from the returned string, then the compiler stop complaining
Appart from the compiler error, everything works at runtime.
Thanks !
Try this:
function userDetails(user: IUser, keys: string[]): string {
let dic: { [prop: string]: string } = <any>user
return keys.map((k: string): string => {
return `- ${k} : ${dic[k]}`
})
.join('\n')
}

Declaring events in a TypeScript class which extends EventEmitter

I have a class extends EventEmitter that can emit event hello. How can I declare the on method with specific event name and listener signature?
class MyClass extends events.EventEmitter {
emitHello(name: string): void {
this.emit('hello', name);
}
// compile error on below line
on(event: 'hello', listener: (name: string) => void): this;
}
Most usable way of doing this, is to use declare:
declare interface MyClass {
on(event: 'hello', listener: (name: string) => void): this;
on(event: string, listener: Function): this;
}
class MyClass extends events.EventEmitter {
emitHello(name: string): void {
this.emit('hello', name);
}
}
Note that if you are exporting your class, both the interface and class have to be declared with the export keyword.
to extend #SergeyK's answer, with this you can get type-checking and completion on both emit and on functions without repeating event types.
Define event listener signatures for each event type:
interface MyClassEvents {
'add': (el: string, wasNew: boolean) => void;
'delete': (changedCount: number) => void;
}
Declare interface which constructs types for MyClass, based on EventListeners (MyClassEvents) function signature:
declare interface MyClass {
on<U extends keyof MyClassEvents>(
event: U, listener: MyClassEvents[U]
): this;
emit<U extends keyof MyClassEvents>(
event: U, ...args: Parameters<MyClassEvents[U]>
): boolean;
}
Simply define you class extending EventEmitter:
class MyClass extends EventEmitter {
constructor() {
super();
}
}
Now you will get type checking for on and emit functions:
Unfortunately you will get completion and type-checking only on those two functions (unless you define more functions inside MyClass interface).
To get more generic solution, you can use this package.
note: it adds no runtime overhead.
import { TypedEmitter } from 'tiny-typed-emitter';
interface MyClassEvents {
'add': (el: string, wasNew: boolean) => void;
'delete': (changedCount: number) => void;
}
class MyClass extends TypedEmitter<MyClassEvents> {
constructor() {
super();
}
}
Here's what I was able to figure out. Overriding the default function with a generic!
interface IEmissions {
connect: () => void
test: (property: string) => void
}
class MyClass extends events.EventEmitter {
private _untypedOn = this.on
private _untypedEmit = this.emit
public on = <K extends keyof IEmissions>(event: K, listener: IEmissions[K]): this => this._untypedOn(event, listener)
public emit = <K extends keyof IEmissions>(event: K, ...args: Parameters<IEmissions[K]>): boolean => this._untypedEmit(event, ...args)
this.emit('test', 'Testing') // This will be typed for you!
}
// Example:
const inst = new MyClass()
inst.on('test', info => console.log(info)) // This will be typed!
You can use typed event emitter package for this.
eg:
import { EventEmitter } from 'tsee';
const events = new EventEmitter<{
foo: (a: number, b: string) => void,
}>();
// foo's arguments is fully type checked
events.emit('foo', 123, 'hello world');
This package also provide interfaces & some utils.
I really liked #Binier's answer and especially the generic solution offered by tiny-typed-emitter. As an alternative, I wrote up this pure-typescript version:
type EmittedEvents = Record<string | symbol, (...args: any) => any>;
export declare interface TypedEventEmitter<Events extends EmittedEvents> {
on<E extends keyof Events>(
event: E, listener: Events[E]
): this;
emit<E extends keyof Events>(
event: E, ...args: Parameters<Events[E]>
): boolean;
}
export class TypedEventEmitter<Events extends EmittedEvents> extends EventEmitter {}
It's used similarly:
type MessageSocketEvents = {
'message': (json: object) => void;
'close': () => void;
};
export class MessageSocket extends TypedEventEmitter<MessageSocketEvents> {
...
}
Use the official typing package for the events library:
npm install events #types/events

Resources