hilla icon indicating copy to clipboard operation
hilla copied to clipboard

Generated nested generic field in DTO looses class type and model file throws an exception

Open KardonskyRoman opened this issue 5 months ago • 6 comments

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

KardonskyRoman avatar Jun 16 '25 11:06 KardonskyRoman

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>>.

platosha avatar Jun 16 '25 11:06 platosha

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).

KardonskyRoman avatar Jun 16 '25 11:06 KardonskyRoman

Using Map and upgrading to 24.8.rc1 helped. Thanks.

KardonskyRoman avatar Jun 16 '25 11:06 KardonskyRoman

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.

KardonskyRoman avatar Jun 17 '25 07:06 KardonskyRoman

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.

cromoteca avatar Jun 17 '25 16:06 cromoteca

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) {
        // ...
    }
}

cromoteca avatar Jun 17 '25 16:06 cromoteca