Property is missing in type which is an interface implemented by class - node.js

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

Related

Restricting the type on function argument in Node.js and TypeScript

Working on a Node.js project and using TypeScript.
I'm trying to restrict a functions argument type to a specific base class. I'm new with both Node & TypeScript and come from a C# background, so likely not quite understanding some of the characteristics of the lang.
Take these snippets.
First, my class declarations
class DTO{
}
class userDTO extends DTO{
#IsDefined({message:"Username required"})
#Expose()
#Length(1,10, {message:"min 1 max 10"})
username:String;
}
class badDTO {
name:String;
}
Now I will create instances:
let user = new userDTO();
user.username = "My username";
let isUserDTO = user instanceof DTO; // Evaluates true
let bad = new badDTO();
bad.name = "Bob";
let isBadDTO = user instanceof DTO; // Evaluates false
Here is the signature of the method I intend to call
export default function ValidateDTO(objToValidate:DTO, validateMissingProperties:boolean): Array<string>{
return [];
}
Finally, when I actually call the function.
let userErrors = ValidateDTO(user, true);
// Why is this allowed?
let badErr = ValidateDTO(bad, true);
I am expecting the 2nd ValidateDTO to show me a warning and not actually run because 'bad' is not a DTO as proven by instanceOf above - if i try passing a string as the 2nd arg I see an error, which is what i expected from passing a non-DTO as the first arg.
Can someone please show me where I am going wrong? How can I restrict the type of object passed into a function.
Happy to share other code as required too. Not sure what else i might be missing.
You're not at all alone being surprised by this. :-) One of the key things about the TypeScript type system is that it's structural (based on structure), not nominal (based on names). As long as something has the minimum structure necessary, it matches even if it has a different ancestry. That means any object will be accepted by the type system as your DTO type because your DTO type has no properties, so all objects match it.
That's mostly a feature, but sometimes you want to disable it. The usual approach when you want to disable it is to use a branding property:
class DTO {
__brand = "DTO" as const;
}
Now, only objects that have a __brand property with the value "DTO" will be allowed where DTO objects are expected by the type system.
Here's a complete example with some minor changes to be more in keeping with JavaScript/TypeScript naming conventions and to supply some bits that were missing in the question code (presumably to keep it short! :-) ):
class DTO {
__brand = "DTO" as const;
}
class UserDTO extends DTO {
/* Commenting these out as they're not relevant to the question.
#IsDefined({message:"Username required"})
#Expose()
#Length(1,10, {message:"min 1 max 10"})
*/
username: string;
constructor(username: string) {
super();
this.username = username;
}
}
class BadDTO {
name: string = "";
}
function validateDTO(objToValidate: DTO, validateMissingProperties: boolean): string[] {
return [];
}
// Okay
validateDTO(new UserDTO("Joe"), true);
// Disallowed by the type system
validateDTO(new BadDTO(), false);
Playground link
Side note 2: In that example I added a constructor to UserDTO that initialized the username property. TypeScript has a shorthand for when you want to use a constructor paramter to initialize an instance property, this is functionally identical to the UserDTO in my example:
class UserDTO extends DTO {
/* Commenting these out as they're not relevant to the question.
#IsDefined({message:"Username required"})
#Expose()
#Length(1,10, {message:"min 1 max 10"})
*/
//−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− note no `username` declaration here
constructor(public username: string) {
// ^−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− note adding `public`
super();
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−− note no code here to do the
// initialization; it's implicit in the `public` declaration above
}
}
Which you use is a matter of style.

How to extend a class in typescript

My service is designed in nodejs.
Below is my scenario
i have two controllers, one will be extending the other. there is a static function in both the controllers where in a static variable will be assigned some value.
depending on the condition of the data, im trying the make a call to the respective controller so that the static variable gets a appropriate assigned value.
Note:
The below code is just a snippet to explain the scenario and not the actual code of the application. But the order / calling / controller structure of this code snippet is exactly same. Also the listOfDept variable will be having separate business logic in the checkStart function of firstController and secondController.
// firstController.ts
firstController implements IFirstController {
private static listOfDept: string[];
static checkStart(){
firstController.listOfDept = // my logic to fill this object
}
constructor (){}
}
getRelevantData(next: (error: string, response: any) => void): void {
var myObject = firstController.listOfDept;
this.myRepository.uniqueData(myObject, next);
}
}
firstController.checkStart();
export = firstController;
//ifirstController.ts
interface IFirstController {
getRelevantData(next: (error: string, response: any) => void): void;
}
// secondController.ts
secondController extends firstController implements iSecondController {
private static listOfDept: string[];
static checkStart(){
firstController.listOfDept = ["Computer Science"];
}
constructor (){
super();
}
}
secondController.checkStart();
export = secondController;
//isecondController.ts
interface ISecondController implements ifirstController{}
//Controller calling the getRelevantData function
//middlewareController
middlewareController implements IMiddlewareController {
constructor(private firstController: IFirstController, private secondController: ISecondController) {
}
getDepData(data: any, next: (error: string, response: any) => void): void {
if(data.url = "fromParent") {
// im expecting this to make a call to checkStart() of firstController
this.firstController.getRelevantData();
} else {
// im expecting this to make a call to checkStart() of secondController
this.secondController.getRelevantData();
}
}
}
Problem faced with the above code
No matter which way the getRelevantData function is getting called, im always getting the value of listOfDept as computer science. It is never going in the checkStart function of first controller.
In general I would discourage using static methods for this kind of initialization and instead inject the required data into constructors or create factory methods for creating object with necessary data.
But, if you do want to use static properties, the problem is that you need to refer to the right parent class in the getRelevantData implementation. The class that constructed the instance can be accessed through constructor property. TypeScript does not process this scenario well, so you have to make a type cast:
// firstController.ts
class firstController implements IFirstController {
// Need to be `protected` to be accessible from subclass
protected static listOfDept: string[];
static checkStart(){
firstController.listOfDept; // my logic to fill this object
}
constructor (){}
getRelevantData(next: (error: string, response: any) => void): void {
// You need to refer to the constructor
let Class = this.constructor as typeof firstController;
var myObject = Class.listOfDept;
// the rest
}
}
firstController.checkStart();
//ifirstController.ts
interface IFirstController {
getRelevantData(next: (error: string, response: any) => void): void;
}
// secondController.ts
class secondController extends firstController implements ISecondController {
// No `listOfDept` definition here
static checkStart(){
secondController.listOfDept = ["Computer Science"];
}
constructor (){
super();
}
}
secondController.checkStart();

TypeScript Node.js this command undefined

I started writing Ts, but now I'm a novice.
export class CrudController<AddDto extends CoreAddDto>{
protected readonly addDtotype: new () => AddDto;
constructor(addDtotype: (new () => AddDto)) {
this.addDtotype = addDtotype;
}
public async add(ctx: any, next: any) {
/// this undefined !!! ///
const dto = new this.addDtotype();
Object.assign(dto, ctx.request.body);
}
}
class FooController extends
CrudController<FooDto> {
constructor {
super(FooDto);
}
}
Why didn't I understand this command undefined?
this works
public add = async (ctx: any, next: any) => { }
method works as a property, why ??
This isn't a bug, your linter is correct because your add method is unbound. Basically there are two types of function declarations in JavaScript, regular function statements and arrow functions, and the primary difference is that regular function statements provide their own definition for this within their scope, while arrow functions binds to the current this in the scope they are defined.
So when a method is a normal function:
add(ctx: any, next: any) {
const dto = new this.addDtotype();
Object.assign(dto, ctx.request.body);
}
this points to add, and not to your CrudController but when you change it to an arrow:
public add = async (ctx: any, next: any) => { }
this is not overridden, and so it still points to your component. Note that you do not need to do this for built-in methods like render or componentDidMount, only for methods you define.
So basically anytime you want to use this in reference to your component while inside a method, you should define it like method = () => {}. If you seriously hate arrow functions, you can also bind methods in the constructor like so:
constructor(props){
super(props)
this.add = this.add.bind(this)
this.method = this.method.bind(this)
}
TSLint has a great rule to help prevent making this mistake (it's an easy one to make) called "no-unbound-method"

custom cast function and abstracts

This code outputs Null<_Test.Bar_Impl_>. I wanted it to output Foo but I see why it does not work that way. But may be I can somehow overcome this limitation.
My primary goal is to create function that will work like cast, but return null instead of throwing exception. And it should work with abstracts.
class Foo {
}
abstract Bar(Foo) {
}
class MyCast {
inline static public function doCast<T>(value: Any, type: Class<T>): Null<T> {
return Std.is(value, type) ? cast value : null;
}
}
class Test {
static function main() {
$type(MyCast.doCast(null, Bar));
}
}
Actually that cannot work at all like that, since Std.is(value, AbstractType) will always fail because the abstract does not exist any more at runtime.
See https://try.haxe.org/#1Afb5, and especially:
Use #:forward to access foo from Bar instances (forward doc)
Use from Foo to safe cast Foo instances into Bar instances (see implicit cast doc) (note that this feature on itself may be exactly what you were trying to achieve: https://try.haxe.org/#cc903)

How to stub a private method of a class written in typescript using sinon

I am writing unit tests for a public method which is, in turn, calling a private method of the class written in typescript (Node JS).
Sample Code
class A {
constructor() {
}
public method1() {
if(this.method2()) {
// Do something
} else {
// Do something else
}
}
private method2() {
return true;
}
}
Now to test method1() I need to stub method2() which is a private method.
here what I am trying :
sinon.stub(A.prototype, "method2");
Typescript is throwing the error :
Argument of type '"method2"' is not assignable to parameter of type '"method1"'
Any help would be appreciated.
Thank You
The problem is that the definition for sinon uses the following definition for the stub function :
interface SinonStubStatic { <T>(obj: T, method: keyof T): SinonStub; }
This means that the second parameter must be the name of a member (a public one) of the T type. This is probably a good restriction generally, but in this case it is a bit too restrictive.
You can get around it by casting to any:
sinon.stub(A.prototype, <any>"method2");
Sometimes when the complexity of code and tests is more significant I prefer to "externalize" private methods. You can do that, that either with a (partial) class or a (partial) interface.
it('private methods test', async () => {
// original class
class A{
public method1():string{
if(this.method2()) {
// Do something
return "true";
} else {
// Do something else
return "false";
}
}
// with private method
private method2():boolean{
return true;
}
}
// interface that makes the private method public
interface IAExternalized{
method2():boolean;
}
// class that makes the private method public
class APrivate implements IAExternalized{
// with public method
method2():boolean{
return true;
};
}
// test before mocking
let test:A = new A();
let result:string = test.method1();
result.should.be.equal("true");
// let's mock the private method, but with typechecking available
let stubMethod2:sinon.SinonStub = sinon.stub(<IAExternalized><unknown>(A.prototype), "method2").returns(false);
result = test.method1();
result.should.not.be.equal("true");
result.should.be.equal("false");
// access private method of an object through public-interface
let testPrivate:IAExternalized = <IAExternalized><unknown>test;
let result2:boolean = testPrivate.method2();
result2.should.not.be.equal(true);
result2.should.be.equal(false);
});
NOTE: If you control the code you are testing, you do not need to double code, prone to mistakes, but you can make your class implement the interface. To convert standard (without private) interface into "externalized" you can extend it with public methods.
export interface IAExternalized extends IAPrivate {
method2():boolean
};

Resources