Typescript Conditional Statement doesn't filter values? - node.js

When I use Typescript Conditional Statement it doesn't remove the values. For example, if the type of "ErrorsType" is string | null, and I use ErrorsType extends string ? InHereTheValueOf'ErrorsType'IsStill'String'|'null'InsteadOf'String' : InHereTheValueOf'ErrorsType'IsStill'String'|'null'InsteadOf'null'
Here is my code:
export const BotErrors = {
test: (reason: string) => `This error bc of ${reason}`,
ValueTooLow: 'You can not reduce value below 0',
};
type ErrorsType = typeof BotErrors;
export default class SomeError<T extends keyof ErrorsType> extends Error {
constructor(code: T, ...args: ErrorsType[T] extends Function ? Parameters<ErrorsType[T]> : []) {
let msg: string | Function = BotErrors[code];
if (typeof msg === 'function') msg = msg(...args) as string;
super(msg);
}
}
At ErrorsType[T] got error: Type 'Function & { test: (reason: string) => string; ValueTooLow: string; }[T]' does not satisfy the constraint '(...args: any) => any'. Because of the BotErrors have both function and string as it's value.
After I add // #ts-nocheck everything is fine, even with type definition. But I'm try wondering if there is any way to not ignore errors?

The problem with
ErrorsType[T] extends Function ? Parameters<ErrorsType[T]> : []
is that the Parameters<T> utility type is defined as
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
where its type parameter is constrained to the function type expression (...args: any) => any,
while you have only checked the ErrorsType[T] type argument against the Function interface. And while (...args: any) => any is assignable to Function, the reverse is not true: just because something is a Function the compiler does not know that it is a (...args: any) => any. The Function type is best avoided in general, since it represents untyped function calls.
Anyway it's easy enough to fix this; just change the conditional type check from Function to (...args: any) => any:
ErrorsType[T] extends (...args: any) => any ? Parameters<ErrorsType[T]> : []
Or the equivalent inlined version:
ErrorsType[T] extends (...args: infer P) => any ? P : []
This makes the error you mentioned go away. Note that once this is resolved your example code has other errors, but those are out of scope for the question as asked.
Playground link to code

Related

Choosing correct component type based on runtime condition

I am looking for some guidance on how best to accomplish my goal. Essentially, I want to choose the correct component type based on a runtime condition(someCondition). However, I am getting the issue below. I am sure I am overlooking something simple. I imagine I need some factory to produce the correct type(not sure why I am having trouble coming up with one 😅).
I would appreciate it if someone would explain the best way to approach this problem for future reference! Thank you in advance!
Types of construct signatures are incompatible. Type 'new <T>(props: Props<T>) => BComp<T>' is not assignable to type 'new <T>(props: Props<T>) => AComp<T>'. Construct signature return types 'BComp<T>' and 'AComp<T>' are incompatible. The types of 'getState' are incompatible between these types. Type '(s: BState) => BState' is not assignable to type '(s: AState) => AState'. Types of parameters 's' and 's' are incompatible. Property 'b' is missing in type 'AState' but required in type 'BState'.ts(2419) typescript_advance_types.ts(23, 5): 'b' is declared here.
Components:
interface Props<T> {
items: T[];
}
interface ComponentState {}
abstract class Component<T, S extends ComponentState> {
constructor(readonly props: Props<T>){}
abstract getState(s:S):S;
}
interface AState extends ComponentState {
a: boolean;
}
class AComp<T> extends Component<T, AState>{
getState(s: AState): AState {
throw new Error("Method not implemented.");
}
}
interface BState extends ComponentState {
b: boolean;
}
class BComp<T> extends Component<T, BState>{
getState(s: BState): BState {
throw new Error("Method not implemented.");
}
}
Conditionally choosing type:
let someCondition = false;
let component = AComp;
if(someCondition){
component = BComp; // issue
}
export const FinalComponent = component;

Property is missing in type which is an interface implemented by class

Node: 17.7.1
Typescript: 4.6.3
I was working with an older repo on DDD and I came across a Typescript error as I was trying to recreate the code, which I am not understanding how to fix.
The IDE "error" occurs in AfterSomethingCreated class when registering with the code of :
Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent.name);
Argument of type '(event: Event) => Promise<void>' is not assignable to parameter of type '(event: IEvent) => void'.
Types of parameters 'event' and 'event' are incompatible.
Property 'something' is missing in type 'IEvent' but required in type 'SomethingCreatedEvent'.ts(2345)
Class SomethingCreatedEvent implements IEvent interface. SomethingCreatedEvent also includes a property in addition to the properties from IEvent. When the property is included, the error is thrown, when taken out, the above error is thrown in the IDE
Code:
IEvent.ts
export interface IEvent {
//.....
}
IHandle.ts
export interface IHandle<IEvent> {
setupSubscriptions(): void;
}
Events.ts
export class Events {
//Methods...
public static register(callback: (event: IEvent) => void, eventClassName: string): void {
//Do Stuff
}
//Methods...
}
SomethingCreatedEvent.ts
export class SomethingCreatedEvent implements IEvent {
//.....
public something: Something;
constructor (something: Something) {
this.something = Something;
//.....
}
//......
}
}
AfterSomethingCreated (Where Error Is Occurring)
export class AfterSomethingCreated implements IHandle<SomethingCreatedEvent> {
constructor () {
this.setupSubscriptions();
}
setupSubscriptions(): void {
---> ERROR -> Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent.name);
}
private async onSomethingCreatedEvent (event: SomethingCreatedEvent): Promise<void> {
//Do stuff
}
}
The error happens because Events.register() takes a callback that supposedly accepts any IEvent whatsoever. Thus it should be perfectly acceptable to actually call the callback with the minimal possible IEvent (in your case since IEvent is an empty interface this is just {}, the empty object):
public static register(callback: (event: IEvent) => void, eventClassName: string): void {
callback({}); // <-- look, no error
}
On the other hand the onSomethingCreatedEvent() method expects that its input will be a SomethingCreatedEvent, and so it should be perfectly acceptable for this method to access event properties unique to SomethingCreatedEvent objects, like the something property (whose value I am assuming is string, since you didn't define the Something type in your code. That is, I'm acting as if type Something = string;):
private async onSomethingCreatedEvent(event: SomethingCreatedEvent): Promise<void> {
console.log(event.something.toUpperCase());
}
But now inside setupSubscriptions() you are passing a callback which only accepts SomethingCreatedEvent events to Events.register(), which is an error:
setupSubscriptions(): void {
Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent.name); // error!
// -----------> ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Argument of type '(event: SomethingCreatedEvent) => Promise<void>' is not
// assignable to parameter of type '(event: IEvent) => void'.
}
And that's an error for good reason. If you call the code as modified above, you get a runtime error because somewhere we're calling a callback with the wrong input:
new AfterSomethingCreated();
// RUNTIME ERROR: Uncaught (in promise) TypeError: event.something is undefined
Since this was existing code, presumably this doesn't actually happen in practice. There's actually a bunch of existing JavaScript which is technically unsafe this way. TypeScript checks method parameters in a bivariant way, meaning that it will allow both safe narrowing and unsafe widening operations. Function parameters are checked more strictly (assuming you have the --strictFunctionTypes compiler option enabled, which is part of the --strict suite of compiler features).
If you want to get the more loosely typed behavior, you need to represent the type of callback as a method instead of a function. By the way, here's the difference:
interface Test {
functionSyntax: (ev: IEvent) => void;
methodSyntax(ev: IEvent): void;
}
const test: Test = {
functionSyntax: (ev: SomethingCreatedEvent) => { }, // error!
methodSyntax: (ev: SomethingCreatedEvent) => { } // okay!
}
See how the declaration of functionSyntax in Test is a property with an arrow function expression type, while methodSyntax looks more like a method declaration. And see how the implementation of test complains about the functionSyntax property accepting too narrow of a type, while the methodSyntax property does not complain.
So if you want to just suppress the error, you can rely on method syntax. Well, it's a little tricky, because there's no method syntax for standalone functions. You can't write (ev: IEvent): void as a type, and {(ev: IEvent): void} is treated like function syntax. The trick here is to make an actual method type and then index into the surrounding object:
type MethodSyntax = { method(event: IEvent): void }["method"]
// type MethodSyntax = (event: IEvent) => void, but marked as a method
And now if you write Events.register() with that:
public static register(callback: MethodSyntax, eventClassName: string): void { }
Then your call will suddenly work with no error:
Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent.name); // okay
This isn't any more type safe, but at least it's not in error.
If you care about enforcing type safety, then you'll probably need to refactor so that nothing bad can happen when a handler handles a callback. Here's one possible approach:
class Events {
static handlers: ((event: IEvent) => void)[] = [];
public static register<T extends IEvent>(
callback: (event: T) => void,
eventClass: new (...args: any) => T
): void {
this.handlers.push(ev => ev instanceof eventClass && callback(ev));
}
public static handleEvent(event: IEvent) {
this.handlers.forEach(h => h(event));
}
}
Now Events.register() is a generic function that accepts a callback that only accepts an event of type T, and an eventClass constructor (instead of a class name) for T. This way each handler can be called for each event... we don't call callback(event) unless event instanceof eventClass. With just a class name, it would be hard for the compiler to verify that any particular event would be appropriate for any particular callback, as the name property of classes is not strongly typed in TypeScript (see microsoft/TypeScript#43325 and issues linked within for more info).
And then the following is accepted now for SomethingCreatedEvent:
setupSubscriptions(): void {
Events.register(this.onSomethingCreatedEvent.bind(this), SomethingCreatedEvent);
}
while something inappropriate would be flagged:
Events.register((o: SomethingCreatedEvent) => { }, Date) // error!
// Property 'something' is missing in type 'Date' but required in type 'SomethingCreatedEvent'.
Playground link to code

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.

Turning map into plain object for TS declaration

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>'.
}
}

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