5

I'm trying to make the following thing,

import { IValueObject } from "../../shared/domain/IValueObject";
import { AbstractNanoidGenerator } from "../../shared/infrastructure/AbstractNanoidGenerator";

export class CompanyID extends AbstractNanoidGenerator implements IValueObject<string, CompanyID> {
  protected constructor(private companyID: string) {
    super();
  }

  get value(): string {
    return this.companyID;
  }

  equals(vo: CompanyID) {
    return vo.companyID === this.companyID;
  }
}

export abstract class AbstractNanoidGenerator {
  static generate<T extends AbstractNanoidGenerator>(this: new (ID: string) => T): T {
    return new this(nanoid());
  }

  static fromString<T extends AbstractNanoidGenerator>(this: new (ID: string) => T, id: string) {
    return new this(id);
  }
}

export interface IValueObject<T, FinalClass> {
  readonly value: T;
  equals(_vo: FinalClass): boolean;
}

It works, but they ask me to declare the constructor of the child class as public. Which I would not do.

The 'this' context of type 'typeof CompanyID' is not assignable to method's 'this' of type 'new (ID: string) => CompanyID'. Cannot assign a 'protected' constructor type to a 'public' constructor type.ts(2684)

Any idea ?

Here is the link to the playground

Regards

5
  • Please share IValueObject Commented Mar 12, 2021 at 11:42
  • Try this example typescriptlang.org/play?#code/… and let me know if it works for you Commented Mar 12, 2021 at 12:01
  • @captain-yossarian Sorry dude, I've changed the code a bit, I was sharing the wrong example. Let's check the edited post Commented Mar 12, 2021 at 13:23
  • I don't have any error :( could you please share it in TS playground? Commented Mar 12, 2021 at 13:27
  • @captain-yossarian Done, in the post directly, keep me in touch :-) Thanks Commented Mar 12, 2021 at 14:30

1 Answer 1

5

The Problem

protected is an access modifier. You cannot put it on a type or interface, since they are public contracts. So, we won't be able to specify protected properties on a type that's going to be used when defining parameters of a function, including this.

In our case, when we say this: new (ID: string) => T, we are saying the passed argument for this (or technically, the "context object" that this function is called on), should be of type new (ID: string) => T. Which only stands for public properties. :(

As of November 2021 (TS 4.5), there still isn't any way to talk about protected properties of an interface (see this comment), and/or specifying protected constructors of a class.

TS 4.2 has add support for abstract Construct Signatures. So, hopefully in the future we might see some support for the protected constructors, as well.

The Hack

We learned that you cannot enforce that the passed this parameter to have a protected constructor. What's the next best alternative, that's better than any? :)

Let's say we are good with assuming that if it's a subclass of AbstractNanoidGenerator, it's always going to have a protected constructor that takes an (ID: string) and returns an instance of itself. (same subclass.)

So our mission would be just enforcing the this param to be "a subclass of AbstractNanoidGenerator" and properly determine the return type to be an instance of that subtype.

Class vs Instance Object

First of all, let's review the difference between the "Class" and "Instance Object"s.

When you say let id: CompanyID, the type of id is An instance of the CompanyID class.

But the this in the static functions above is not supposed to be this: CompanyID which would imply we need an instance! We want the actual CompanyID class itself to be passed/used, which has the type of typeof CompanyID (see this answer). :)

How should we say we want the class itself?

There are two main ways in TypeScript to have the type for a "Class". That would be used to enforce the this parameter in our case.

Class Type Version 1: Using new

The most common way to imply something is a "Class" is to say if you call new on that, it's gonna produce an "Instance". That inspires (this type of solutions) to go with the following utility functions.

export interface Type<T> extends Function {
    new (...args: any[]): T;
}

// --- Or, similarly ---
export type Constructor<VALUE_T = any> = new (...args: any[]) => VALUE_T;

Class Type Version 2: Using prototype

The problem with the above version is that it doesn't work for classes with protected constructor, like our CompanyID. Such classes don't have a public constructor to be picked up by new, and will fail there.

That's why in our second way we rely on having a property called prototype.

export type ClassDefinitionFor<T> = { prototype: T };

(So, let theClass:ClassDefinitionFor<CompanyID> = CompanyID works!)

and to reverse that...

type InstanceOfClass<T> = T extends { prototype: infer R } ? R : never;

What should be the params now?

Given the two utility functions above, I wrote the following.

Note that using as unknown as trick, we are telling TS to consider that just because this is a Class object that extends AbstractNanoidGenerator, it has a constructor that produces an object, with the type on the prototype of that class.

type ClassDefinitionFor<T>  = { prototype: T };
type InstanceOfClass<T>     = T extends { prototype: infer R } ? R : never;

export abstract class AbstractNanoidGenerator {
  static generate<
    INSTANCE_T extends AbstractNanoidGenerator,
    CLASS_T extends ClassDefinitionFor<INSTANCE_T>,
    CTR_T extends new (ID: string) => InstanceOfClass<CLASS_T>,
  >(this: CLASS_T): InstanceOfClass<CLASS_T> {
    return new (this as unknown as CTR_T)(Math.random().toString());
  }
}

(Full TS Playground with extra tests.)

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.