The Issue
protected
serves as an access modifier and cannot be applied to a type or an interface since they represent public contracts. Therefore, it is not possible to define protected properties for a type intended for function parameters, including this
.
In the scenario of this: new (ID: string) => T
, it specifies that the this
argument must be of type new (ID: string) => T
, which exclusively allows access to public properties.
As of November 2021 (TS 4.5), TypeScript still lacks the capability to reference protected
properties within an interface or to define protected
constructors within a class (source). There is hope for future support concerning protected constructors with the introduction of abstract
Construct Signatures in TS 4.2 (details).
The Workaround
Understanding that enforcing a protected constructor for the passed this
parameter is unattainable, what superior alternative exists beyond any
? :)
Consider assuming that any subclass of AbstractNanoidGenerator
invariably possesses a protected constructor which accepts an (ID: string)
argument and returns an instance of itself (similar subclass).
Consequently, our objective shifts to mandating that the this
parameter is "a subclass of AbstractNanoidGenerator
" and accurately specifying the return type as an instance of that subtype.
Class vs. Instance Object
Let's differentiate between a "Class" and "Instance Object".
When declaring let id: CompanyID
, the type of id
represents an instance of the CompanyID class.
However, the this
in the static functions above should not be equated to this: CompanyID
, which would imply a need for an instance. Rather, we require the actual CompanyID
class itself to be passed/used, characterized by the type typeof CompanyID
(refer to this explanation). :)
How to Determine the Class Itself?
In TypeScript, there are two primary methods to specify a type as a "Class" for enforcing the this
parameter in our context.
Class Type Approach 1: Leveraging new
A prevalent approach to indicating a "Class" involves verifying that invoking new
on the type results in an "Instance". This concept inspired (these solutions) and led to the following utility functions.
export interface Type<T> extends Function {
new (...args: any[]): T;
}
// --- Alternatively ---
export type Constructor<VALUE_T = any> = new (...args: any[]) => VALUE_T;
Class Type Approach 2: Utilizing prototype
The prior version may not function for classes with protected constructors like our CompanyID
. Such classes lack a public constructor to be captured by new
, resulting in failure.
Hence, the alternative relies on the existence of a property named prototype
.
export type ClassDefinitionFor<T> = { prototype: T };
(Therefore,
let theClass:ClassDefinitionFor<CompanyID> = CompanyID
functions as intended!)
Conversely...
type InstanceOfClass<T> = T extends { prototype: infer R } ? R : never;
Refining the Parameters
Applying the above utility functions, the subsequent code is formulated.
By using the as unknown as
workaround, TypeScript recognizes that the this
object is a Class object extending AbstractNanoidGenerator
, possessing a constructor that produces an object with the type defined 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());
}
}
(Access the complete TS Playground with supplementary tests.)