I've created a unique decorator for classes that accepts an initializer function as its argument. Inside this `initializer` function, I want to be able to return an instance that matches the class type or a derived one:
function JsonObject<T>(initializer: (json: any) => T) {
return function (target: { new (...args: any[]): T }) {
// ...
}
}
@JsonObject(function (json) {
return new Animal();
})
class Animal {
name: string;
}
Returning an instance of the exact class works fine, but...
Detailed Overview
When trying to return an instance of a derived class, it doesn't work. While I can return a base instance, I cannot return a derived one. For example, returning a `Cat` results in an error:
@JsonObject(function (json) {
return new Cat(); // Error.
})
class Animal{
name: string;
}
class Cat extends Animal {
color: string;
}
... even though a Cat is an Animal. However, it's possible to return an Animal instead of a Cat (even though incorrect), for a Cat:
@JsonObject(function (json) {
return new Animal(); // OK, but it shouldn't be
})
class Cat extends Animal {
color: string;
}
In-depth Explanation
The JsonObject Decorator Factory
The `JsonObject` function acts like a callback function with a generic type parameter `T`, where it returns a `T`. It then returns a function accepting a newable type that returns a `T`. This returned function is the actual class decorator.
The compiler enforces correct types in the `initializer` function, preventing mismatched types like returning a string.
Issue with Subtypes
When using subtypes, the behavior is opposite. I can return a base type from the `initializer` function, but not a derived type. An error occurs when used on the middle class of a 2-step inheritance pattern:
@JsonObject(function (json) {
// Test case: return a base type.
return new Animal(); // OK, but it shouldn't be: an 'Animal' is not a 'Cat'
})
@JsonObject(function (json) {
// Test case: return an exact corresponding type.
return new Cat(); // OK, as it should be
})
@JsonObject(function (json) {
// Test case: return a derived type.
return new Kitty(); // <-- Error, but it should be OK, a Kitty *is* a Cat
})
class Cat extends Animal {
color: string;
}
class Kitty extends Cat {
cutenessFactor: number;
}
Error: Type 'Cat' is not assignable to type 'Kitty'. Property 'cutenessFactor' is missing in type 'Cat'.
The issue arises from the compiler inferring generics based on the `initializer`, causing errors. The solution lies in inferring `T` from the "return" type of `target`. Explicitly specifying the generic type parameter resolves the issue, although redundantly:
@JsonObject<Cat>(function (json) {
return new Kitty(); // OK, since type 'Kitty' is assignable to type 'Cat'
})
class Cat extends Animal { }