I was in need of a way to change the parameters and return type of a class function if a field from this class is true, taking into account that i have an abstract class and a bunch (more than 6) of classes that will extend it and have this field set to true or not, so for example i have this code
abstract class AbstractClass<TSupportsBatch extends boolean = false> {
public batchSupport: TSupportsBatch
public supportsBatchTracking(): this is AbstractClass<true> {
return !!this.batchSupport
}
// If TSupportsBatch is true, the parameter will be an array if not an individual item
public abstract sendItems(item: TSupportsBatch extends true ? { id: number }[] : { id: number }): void
}
class ConcreteClass1 extends AbstractClass<true> {
public batchSupport: true = true
public override sendItems(batch: unknown[]): void {
// Process batch and send it
}
}
class ConcreteClass2 extends AbstractClass {
public override sendItems(item: unknown): void {
// Send it individually
}
}
const handleInstance = (instance: AbstractClass) => {
const items = [
{ id: 1 },
{ id: 2 },
{ id: 3 },
]
// Narrowing using only the field batchSupport doesn't work, so i needed a predicate function there
if (instance.supportsBatchTracking()) {
instance.sendItems(items) // Error because instance type is never here
} else {
for (const item of items) {
instance.sendItems(item)
}
}
}
When i try narrowing the instance of the extended class to get the function supporting a batch as parameter the instance turns into never, does someone know a way to solve this or if this is a bug from typescript ?
The point is the moment any generic type has been referenced with a specific type, the whole generic type will become specific with respect to the type passed in.
It means this :
const instanceFalse : AbstractClass
;
instanceFalse will become the specific type below. Please note the default type for the type parameter is false over here. Please see the conditional type has also been evaluated to the type specific type { id: number } which has fixed the interface of the method sendItems in a specific way.
abstract class AbstractClassFalse{
public batchSupport: false
public abstract sendItems(item: { id: number }): void
public supportsBatchTracking(): this is AbstractClass<true> {
return !!this.batchSupport
}
}
Similarly,
const instanceTrue : AbstractClass<true>
;
Please note sendItems has also changed specifically.
abstract class AbstractClassTrue{
public batchSupport: true
public abstract sendItems(item: { id: number }[]): void
public supportsBatchTracking(): this is AbstractClass<true> {
return !!this.batchSupport
}
}
However, the true and false branches of the type narrowing code below, tries to refer both kinds of interfaces in sendItems which is the cause of failure. The issue is not really specific to the type guard. This can be confirmed by the code underneath.
if (instance.supportsBatchTracking()) {
instance.sendItems(items) // Error because instance type is never here
} else {
for (const item of items) {
instance.sendItems(item)
}
}
Please note instance now is of the type AbstractClass<true>
, as a result, the error has been shifted to the else part.
const handleInstance = (instance: AbstractClass<true>) => {
...
if (instance.supportsBatchTracking()) {
instance.sendItems(items)
} else {
for (const item of items) {
instance.sendItems(item) // Error because instance type is never here
}
}
}
In order to resolve the code in handleInstance fully, both kinds of AbstractClasses must be available in the code. That is the reason the other colleagues have suggested to go for a union. This union will make sendItems wider as below. It means sendItem will now accept either a single object or an array of objects. As a result, both kinds of references in the code will be resolved successfully.
sendItems(item: {
id: number;
}[] & {
id: number;
}): void
There is another definite advantage in going for this kind of union. The type guard based on the type predicate may be avoided, instead the property batchSupport may be used as a discriminant property. Therefore, a type guard can be built on the same.
if (instance.batchSupport) {
instance.sendItems(items)
} else {
for (const item of items) {
instance.sendItems(item)
}
}