typescriptgenericsnullable-reference-typesreinforced-typings

Reinforced.Typings and Generic Nullable Reference Type information


My issue is very similar to this issue.

I define some server model like this:

[TsInterface]
public class GenericValue<T>
{
    public int Key { get; set; }

    public T? Value { get; set; }
}

It is properly defined in TypeScript as follows

export interface GenericValue<T>
{
  Key: number;
  Value?: T;
}

I use it like so (from C#)

[TsInterface]
public class DTO
{
    public GenericValue<string?> SomeProperty { get; set; } = null!
}

I would expect the TypeScript to be written as:

export inteface DTO
{
    SomeProperty: IGenericValue<string | undefined>;
}

instead it is written in TypeScript as

export inteface DTO
{
    SomeProperty: IGenericValue<string>;
}

I do not believe this is in agreement with the nullable annotation in the type.

I think the problem is that RtSimpleTypeName does not retain the information on if the type was nullable when its constructed, as a result the code which outputs the generic type information into the AST does not have a way to determine the nullability.

See here, it seems the ToString method is called and it missed already the ability to pass the NRT information to the constructed RtSimpleTypeName.

I don't think I can implement this using my own Visitor because of how SerializeGenericArguments is called, there is no way to determine if the type was nullable or not?

This is evident by looking at the ToString method of RtSimpleName

Here is a naive implementation which shows what I am trying to do but only with respect to nullability which I can't seem to do without changing things around in the library.

public sealed class NRTTypeScriptExportVisitor(
    TextWriter writer,
    ExportContext exportContext)
    : TypeScriptExportVisitor(writer, exportContext)
{
    private bool isWritingMembers;
   
    public override void Visit(RtInterface node)
    {
        if (node == null)
            return;

        Visit(node.Documentation);
        var context = Context;
        Context = WriterContext.Interface;
        AppendTabs();
        if (node.Export)
            Write("export ");

        Write("interface ");
        Visit(node.Name);
        if (node.Implementees.Count > 0)
        {
            Write(" extends ");
            SequentialVisit(node.Implementees, ", ");
        }

        Br();
        AppendTabs();
        Write("{");
        Br();
        Tab();

        isWritingMembers = true;

        foreach (var item in DoSortMembers(node.Members))
            Visit(item);

        isWritingMembers = false;

        UnTab();
        AppendTabs();
        WriteLine("}");
        Context = context;
    }

    public override void Visit(RtSimpleTypeName node)
    {
        if (node.HasPrefix)
        {
            Write(node.Prefix);
            Write(".");
        }

        Write(node.TypeName);

        if (node.GenericArguments.Length != 0)
        {
            Write("<");
            for (var i = 0; i < node.GenericArguments.Length; ++i)
            {
                var genericArgument = node.GenericArguments[i];

                Visit(genericArgument);

                if (isWritingMembers) //Ideally I could check genericArgument.IsNullable here
                    Write(" | undefined");

                if (i < node.GenericArguments.Length - 1)
                    Write(", ");
            }
            Write(">");
        }
    }
}

Am I missing something WRT to the visitor?

When looking into the code to see where and if I could make a change to get this to work I saw GetNullableAttributeValue is only checking the top level property?

Doesn't this need to also recurse into the type if its generic?

I am not sure it will help on its own though because it seems the RtSimpleName type would also need to be modified to store this NRT information so it can be used by the Visitors...

Am I incorrect or is there a better way to achieve the desired result?


Solution

  • I was able to make a patch at GenerateNode in PropertyCodeGenerator:

    result.Type = type;
    
    //Add this code
    if (element.GenericReturnTypeIsNullable())
    {
        var simpleType = type as RtSimpleTypeName;
        var genericArguments = simpleType.GenericArguments.Cast<RtSimpleTypeName>().ToArray();
        for (int i = 0; i < genericArguments.Length; i++)
        {
            var genericArgument = genericArguments[i];
            genericArguments[i] = new RtSimpleTypeName(genericArgument.TypeName + " | undefined");                    
        }
        result.Type = new RtSimpleTypeName(simpleType.TypeName, genericArguments);
    }
    

    This proves what I want to achieve is possible but unfortunately means I will have to make my own version of the library to accommodate this change.

    Keep in mind the solution above is not 100% complete as it doesn't check the index of each generic argument, it only assumes if one is nullable then all are :) I leave this as an exercise for the readers...

    If there is a better way please let me know so I am not reinventing the wheel.

    Thank you!