Choosing correct component type based on runtime condition - typescript-typings

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;

Related

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

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.

TypeScript: Decorating a derived class with typed decorator function

I am trying to build an "Entity Framework"-like ORM library for node.js and MongoDB with TypeScript.
With this library, the consumer will be able to define a Model class (ex. Person) that will extend a Base class, and the class decorator will add additional data to the Person class (for example, instances array, that will contain all the Model's instances, and collection_name that will be the MongoDB collection name for the model, etc.).
Code in TypeScript playground.
So my first step was creating a Base class:
class Base<T>
{
static collection_name: string
static instances: Base<any>[]
_id: string
}
So the user will be able to define his model like so:
#decorator<Person>({collection_name: 'people'})
class Person extends Base<Person>
{
#field name
#field address
...
}
then I created a decorator class to set the collection_name and instances properties on the Person class:
function decorator<T>(config: { collection_name: string }) {
return function <T extends Base<T>>(Class: IClass<T>) {
Class.collection_name = config.collection_name;
Class.instances = [];
return Class
}
}
the decorator function receives the user-generated Class, and I am trying to create an interface that will describe the type of such class. I called it IClass:
interface IClass<T>
{
new(): Base<T>
instances: Base<T>[];
collection_name: string
}
new is the constructor (that returns a Base instance)
instances and collection_name are static properties of Base<T> and are non-static here (I'm not sure about this, is this right?)
However, when trying to define the user Model I get the following error:
#decorator<Person>({collection_name: 'people'}) // <==== ERROR HERE ===
class Person extends Base<Person>
{
}
Error:(23, 2) TS2345: Argument of type 'typeof Person' is not
assignable to parameter of type 'IClass>'.
Property 'instances' is missing in type 'typeof Person' but
Type 'typeof Person' is missing the following properties from type
required in type 'IClass>'.
It seems like the typescript compiler is ignoring the static members inherited from Base when checking the type of typeof Person.
How can I define the type of the Class property of the decorator function ?
The problem as jcalz points out is that your decorator is accepting a Class of a type that already has the static properties instances and collection_name. You need to use two different interfaces, one which is a type that simply constructs instances of T with the new(): T signature, and another that extends this interface to include the static properties your decorator will add.
class Base<T> {
static _id = 0;
private _id: number;
constructor () {
this._id = Base._id++;
}
}
interface BaseConstructor<T extends Base<T>> {
_id: number;
new(): T;
}
interface DecoratedBaseConstructor<T extends Base<T>> extends BaseConstructor<T> {
instances: T[];
collection_name: string;
}
function decorator<T extends Base<T>>(config: { collection_name: string }) {
return (baseConstructor: BaseConstructor<T>): DecoratedBaseConstructor<T> => {
const decoratedBaseConstructor = baseConstructor as Partial<DecoratedBaseConstructor<T>>;
decoratedBaseConstructor.collection_name = config.collection_name;
decoratedBaseConstructor.instances = [];
return decoratedBaseConstructor as DecoratedBaseConstructor<T>;
};
}
#decorator<Person>({collection_name: 'people'})
class Person extends Base<Person> {
name: string;
constructor () {
super();
this.name = 'foo';
}
}
With this approach, all of Base's static members must be public. Any static members of Base initialized in the decorator should go in the DecoratedBaseConstructor, and any remaining static members not initialized in the decorator should go in the BaseConstructor instead.
I assume that you use the generic type T in the Base class somehow in your actual code, but if you don't, you should remove the generic type from the Base class and everything else will still work the same.
Check out the above snippet in this playground.

Typescript: Override static factory method of parent Class in Child method

I'm running into some problems with dependency injection with Typescript. On every Class that I add a factory static method where all dependencies are set. I do this for testing purposes so that I'm still able to use the TDD approach.
Now I'm running into some problems with overriding the factory method of the parent class in a child class. Example:
interface DepsA {
a: string
}
interface DepsB extends DepsA {
b: Child1
}
class Parent {
constructor(protected deps: DepsA | DepsB) {}
public static factory<T extends Parent>() {
return new this({a: 'This is a nice Dependency'}) as T
}
}
class Child1 extends Parent {}
class Child2 extends Parent {
public static factory() {
return new this({a: 'This is a nice Dependency', b: Child1.factory()})
}
}
const child1 = Child1.factory<Child1>()
const child2 = Child2.factory()
The error what I receive is:
[ts]
Class static side 'typeof Child2' incorrectly extends base class static side 'typeof Parent'.
Types of property 'factory' are incompatible.
Type '() => Child2' is not assignable to type '<T extends Parent>() => T'.
Type 'Child2' is not assignable to type 'T'.
I know why I get the error, but have at this point no idea anymore how to fix it, otherwise than renaming the factory static method in Child2.
UPDATE: A Related bug report to this problem, that explains automatically why I use a Generic on the factory method is: #26298
First, there's a pre-defined conditional type called InstanceType which could help you to infer the class type from the static member:
public static factory<T extends typeof Parent>(this: T) {
return new this({ a: 'This is a nice Dependency' }) as InstanceType<T>
}
Second, if you override a method, static or not, in a child class, it should have a compatible signature, including the generic stuff.
Consequently, your code block could look like this (see in Typescript Playground):
interface DepsA {
a: string
}
interface DepsB extends DepsA {
b: Child1
}
class Parent {
constructor(public deps: DepsA | DepsB) {}
public static factory<T extends typeof Parent>(this: T) {
return new this({ a: 'This is a nice Dependency' }) as InstanceType<T>
}
}
class Child1 extends Parent {}
class Child2 extends Parent {
public static factory<T extends typeof Parent>(this: T) {
return new this({a: 'This is a nice Dependency', b: Child1.factory()}) as InstanceType<T>
}
}
const child1 = Child1.factory() // Type: Child1
const child2 = Child2.factory() // Type: Child2
From there, returning the proper deps type, rather than an union, would also be possible in non static members, using as this["deps"]. But you'd have to revamp a bit your code.
Hope it helps ;-)

Unification and implicit cast of the type parameter

class Base, and class Ext extends Base.
class B<T> with typed method foo<T>(value:T)
Why B<Base>.foo doest not accept instance of B<Ext> (implicit downcast of the type parameter?) by default?
Here is an example
http://try.haxe.org/#d443f
class Test {
static function main() {
var bExt = new B(new Ext());
var bBase = new B(new Base());
bBase.foo(bExt);
//ofc
//bBase.foo(cast bExt);
}
}
class B<T>
{
public function new(v:T)
{
}
public function foo(v:B<T>)
{
//
}
}
class Base {
public function new(){}
}
class Ext extends Base {
public function new(){
super();
}
}
Is there any way to trigger implicit cast of the type parameter for B.foo?
There are three ways to interpret and answer your question:
1. foo(v:B<T>):
This is your example and it doesn't compile because T isn't allowed to be be variant. It happens because of the very existence of foo and because allowing bBase.foo(bExt), that is, unifying bExt with bBase, will then allow bBaseOfbExt.foo(bBase).
It is the fact that foo exists and that it can potentially modify the type that makes the bExt unification with bBase unsafe; you can see a similar (but maybe clearer) explanation in the manual, using arrays: type system – variance.
2. foo(v:T):
This is closer to what's on the body of your question (but not in the example) and it works fine.
3. foo<A>(v:B<A>):
Finally, if you have a type parameterized method, it also works, but you'd probably face other variance issues elsewhere.

Resources