Skip to content

Default type argument handling added in 53fa22e is buggy #2823

Closed
@hoodmane

Description

@hoodmane

The changes in #2820 have a couple of shortcomings. The new logic removes a type argument if it matches the default value of any type parameter. What we have to do is work from the last type argument backwards and check each one in turn for whether it matches the default. We need to include all arguments before the last nondefault argument even if they are defaults.

interface T<A, B=number, C=string> {a: A, b: B, c: C}

// Expected:    f0(a: T<number, number, boolean>): T<number, number, boolean>
// Got     :    f0(a: T<boolean>): T<number, number, boolean>
export function f0(a: T<number, number, boolean>): T<number, number, boolean> { return a; }

// Expected:    f1(a: T<string>): T<string>
// Got     :    f1(a: T): T<string>
export function f1(a: T<string>): T<string> { return a; } 

// Expected:    f2(a: T<number>): T<number>
// Got     :    f2(a: T): T<number>
export function f2(a: T<number>): T<number> { return a; }

// Expected:    f3(a: T<number>): T<number>
// Got     :    f3(a: T): T<number, number>
export function f3(a: T<number, number>): T<number, number> { return a; }

// Expected:    f4(a: T<number, string>): T<number, string>
// Got     :    f4(a: T): T<number, string>
export function f4(a: T<number, string>): T<number, string> { return a; }

// Expected:    f5(a: T<string, string>): T<string, string>
// Got     :    f5(a: T): T<string, string>
export function f5(a: T<string, string>): T<string, string> { return a; }

// Expected:    f6(a: T<number, string>): T<number, string>
// Got     :    f6(a: T): T<number, string, string>
export function f6(a: T<number, string, string>): T<number, string, string> { return a; }

// Expected:    f7(a: T<number>): T<number>
// Got     :    f7(a: T): T<number, number, string>
export function f7(a: T<number, number, string>): T<number, number, string> { return a; }

The following patch mostly fixes it, though it leaves the very minor inconsistency that the return type always shows exactly what the source code contains whereas the parameter types will omit defaults that were unnecessarily included in the source code.

--- a/src/lib/converter/types.ts
+++ b/src/lib/converter/types.ts
@@ -824,23 +824,24 @@ const referenceConverter: TypeConverter<
                 convertType(context, (type as ts.StringMappingType).type),
             ];
         } else {
-            // Default type arguments are filled with a reference to the default
-            // type. As TS doesn't support specifying earlier defaults, we know
-            // that this will only filter out type arguments which aren't specified
-            // by the user.
-            let ignoredArgs: ts.Type[] | undefined;
-            if (isTypeReference(type)) {
-                ignoredArgs = type.target.typeParameters
-                    ?.map((p) => p.getDefault())
-                    .filter((x) => !!x);
-            }
-
             const args = type.aliasSymbol
                 ? type.aliasTypeArguments
                 : (type as ts.TypeReference).typeArguments;
 
+            let idx;
+            if (args && isTypeReference(type)) {
+                idx = args.length - 1;
+                while (
+                    idx >= 0 &&
+                    args?.at(idx) ===
+                        type.target.typeParameters?.at(idx)?.getDefault()
+                ) {
+                    idx--;
+                }
+            }
+
             ref.typeArguments = args
-                ?.filter((ref) => !ignoredArgs?.includes(ref))
+                ?.slice(0, idx! + 1)
                 .map((ref) => convertType(context, ref));
         }
         return ref;

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions