hilla
hilla copied to clipboard
Generated nested generic field in DTO looses class type and model file throws an exception
Describe the bug
Hello. This is DTO class:
public class DTO{
List<Pair<Integer, FullQueueStat>> q
}
This is how TS DTO generated:
interface DTO {
q?: Array<Pair_1<number | undefined> | undefined>;
}
So, type FullQueueStat is lost. And the problem is - DTOModel throws en error:
Type 'ArrayModel<PairModel<Pair<unknown, unknown>>>' is not assignable to type 'ChildModel<T, "q">'.
Types of property '['constructor']' are incompatible.
Type '(abstract new (parent: ModelParent, key: string | number | symbol, optional: boolean, options?: ModelOptions<Pair<unknown, unknown>[]> | undefined) => AbstractModel<Pair<unknown, unknown>[]>) & { ...; }' is not assignable to type '(abstract new (parent: ModelParent, key: string | number | symbol, optional: boolean, options?: ModelOptions<NonNullable<T["q"]>> | undefined) => AbstractModel<...>) & { ...; }'.
Types of parameters 'options' and 'options' are incompatible.
Type 'ModelOptions<NonNullable<T["q"]>> | undefined' is not assignable to type 'ModelOptions<Pair<unknown, unknown>[]> | undefined'.
Type 'ModelOptions<NonNullable<T["q"]>>' is not assignable to type 'ModelOptions<Pair<unknown, unknown>[]>'.
Type 'NonNullable<T["q"]>' is not assignable to type 'Pair<unknown, unknown>[]'.
Type 'Pair<number | undefined, unknown> | undefined' is not assignable to type 'Pair<unknown, unknown>'.
Type 'undefined' is not assignable to type 'Pair<unknown, unknown>'.
Expected-behavior
Generated TS DTO is:
interface DTO {
q?: Array<Pair_1<number | FullQueueStat> | undefined>;
}
And generated DTOModel is a correct TS file.
Reproduction
use example from description
System Info
Hilla 24.7.5
Hi, thanks for the issue.
Most likely the cause is lacking support for Pair with two generic arguments.
As a quick workaround, I'd suggest using a Map<String, FullQueueStat> type instead if list of pairs. Maps are limited to string keys due to JSON format limitations. However, since the order of object keys is always preserved in JSON and JS, I think it should not have any major drawbacks compared with List<Pair<Integer, T>>.
I will try. Initially, the object is map, but I had some problems (may be same) with map too. What is why I converted it to Pair (it helped in other cases).
Using Map and upgrading to 24.8.rc1 helped. Thanks.
JFY: If I use type List<Pair<Integer, FullQueueStat>> as return type in Hilla Enpoint method - it works correct. The problem appears when this type is inside DTO.
I was able to partially replicate this bug on 24.8.0.rc1. I created this service:
@BrowserCallable
@AnonymousAllowed
public class TempService {
public record Pair<K, V>(K key, V value) {}
public record FullQueueStat(int size, int maxSize, int waitingTime) {}
public class DTO {
public List<Pair<Integer, FullQueueStat>> q;
}
public void useDto(DTO dto) {
// ...
}
}
Generated TS interface is OK:
interface Pair<K = unknown, V = unknown> {
key: K;
value: V;
}
interface DTO {
q: Array<Pair_1<number, FullQueueStat_1>>;
}
Model, though, seems broken:
class DTOModel<T extends DTO_1 = DTO_1> extends ObjectModel_1<T> {
static override createEmptyValue = makeObjectEmptyValueCreator_1(DTOModel);
get q(): ArrayModel_1<PairModel_1> {
return this[_getPropertyModel_1]("q", (parent, key) => new ArrayModel_1(parent, key, false, (parent, key) => new PairModel_1(parent, key, false), { meta: { javaType: "java.util.List" } }));
}
}
The error is:
Type 'ArrayModel<PairModel<Pair<unknown, unknown>>>' is not assignable to type 'ChildModel<T, "q">'.
The types returned by 'new ['constructor'](...)' are incompatible between these types.
Type 'AbstractModel<Pair<unknown, unknown>[]>' is not assignable to type 'AbstractModel<NonNullable<T["q"]>>'.
Type 'Pair<unknown, unknown>[]' is not assignable to type 'NonNullable<T["q"]>'.
Type 'Pair<unknown, unknown>[]' is not assignable to type 'T["q"]'.
'T["q"]' could be instantiated with an arbitrary type which could be unrelated to 'Pair<unknown, unknown>[]'.ts(2322)
Models.d.ts(79, 96): The expected type comes from the return type of this signature.
Nullability is not the cause: with our without @NonNullApi, the error is always there.
Another possible workaround is to avoid tuple-like classes, that aren't good practice in my opinion since the advent of records. Thus, create a record for each situation where you need those tuples. The service would become something like:
@BrowserCallable
@AnonymousAllowed
public class TempService {
// this can also be a class or an interface, it doesn't matter here
public record FullQueueStat(int size, int maxSize, int waitingTime) {}
// this is the tuple replacement
public record IdStat(Integer id, FullQueueStat stat) {}
public class DTO {
public List<IdStat> q;
}
public void useDto(DTO dto) {
// ...
}
}