Microsoft/TypeScript

Misleading error for a generic parameter with a default involving `this`

Open

#61,812 opened on Jun 4, 2025

View on GitHub
 (0 comments) (0 reactions) (0 assignees)TypeScript (48,455 stars) (6,726 forks)batch import
Domain: This-TypingHelp WantedPossible Improvement

Description

🔎 Search Terms

generic type parameter default, assignability, this

🕗 Version & Regression Information

  • This changed in a9ad94ab3c35615b344c51e721f77655c225b524

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.8.3#code/KYDwDg9gTgLgBAbwL4G4BQaAmwDGAbAQymDnwIGdy4ARCHAVwFtgA7GAHloebYDkDmAPkRo4cFgOAAuGnSasY-ZujHkAliwDmeYDAgtyMmAAs15dEgwaYwKADMCOEgEEWATy7y2I1Ru279QzgTMwsMbDJiUkJKOABRRgAjYExsTABhCDwdHBg1fWpgPBgCADE1Isx2UXidHhhyyoAVNzBgABoagHkAawBGOFAbFkwqT3r2AndhAF5a4HrGvEwWtsGQYdG4Kbc4AH5ZbgV2OLqFJZXW4GEZFmAAN1tOsV6AJnXNqlcPOXq4OfGx1OC3OFWWq2uzzgACEoBAeqwPqwtt9Ad45sDFmDLmshsiqDt9ocvBxMaDmlcbuIHrYagB6OlwCFwADkZLYF2ZeJGBPcRLRpLOHOxEKpd0eUBZcEwEGAVBYEHg5AIeXIdl2JhIOECMCgBGsrNRvwULIAdPTGcyWQKTkKGiLKVKzOJFdtKGpNBJEjpghBgldDe4BWaLWIxBCqBA7HAwHC2rBdiz1FodHoDFKiCQNNrGGAVWpvcBzWIGWGw1abez7RS2oInfLXRR1J6CIXff61my7ZyA9ytoSDpXuw7a3BbjTJebhMggA

💻 Code

Here's what essentially was trying to be written in an understandable way:

type Validator = (data: unknown) => ValidationResult<unknown>;

interface Cloneable {
  data: unknown;
  clone(): this;
}

class ValidationResult<T> implements Cloneable {
  constructor(public data: T) {
    this.isValid = true; // Actual validation logic left off.
  }

  clone(): this {
    return structuredClone(this);
  }

  isValid: boolean;
}

class Collection<V extends Validator, C extends Cloneable = ReturnType<V>> {
// Type 'ReturnType<V>' does not satisfy the constraint 'Cloneable'.
//   Type 'ValidationResult<unknown>' is not assignable to type 'Cloneable'.
//     The types returned by 'clone()' are incompatible between these types.
//       Type 'ValidationResult<unknown>' is not assignable to type 'ReturnType<V>'.
  constructor(validator: V, items: unknown[]) { } // Actual validation logic left off.

  items: C[] = [];
}

Here's the a more distilled way of testing interesting cases:

export {};

declare class Document<DocumentName> {
  name: DocumentName;
  singletons: this;
}

interface AnyDocument {
  singletons: this;
}

declare class EmbeddedCollectionDeltaField<
  ElementFieldType,
  Ok1 extends Document<any> = ElementFieldType extends any ? Document<ElementFieldType> : never,
  Ok2 extends AnyDocument = Document<ElementFieldType>,
  Broken extends AnyDocument = ElementFieldType extends any ? Document<ElementFieldType> : never
  // Type 'ElementFieldType extends any ? Document<ElementFieldType> : never' does not satisfy the constraint 'AnyDocument'.
  //   Type 'Document<ElementFieldType>' is not assignable to type 'AnyDocument'.
  //    Types of property 'singletons' are incompatible.
  //      Type 'Document<ElementFieldType>' is not assignable to type 'ElementFieldType extends any ? Document<ElementFieldType> : never'.
> {}

Finally this playground has the most confusing error.

🙁 Actual behavior

Multiple errors.

🙂 Expected behavior

No error. Or at least a less misleading error message. Specifically the lines Type 'ValidationResult<unknown>' is not assignable to type 'Cloneable'. and Type 'Document<ElementFieldType>' is not assignable to type 'AnyDocument'. are both testably false.

Additional information about the issue

The reason for this misleading error appears to be this snippet:

        if (constraintType && defaultType) {
          checkTypeAssignableTo(defaultType, getTypeWithThisArgument(instantiateType(constraintType, makeUnaryTypeMapper(typeParameter, defaultType)), defaultType), node.default, Diagnostics.Type_0_does_not_satisfy_the_constraint_1);
        }

While the types are displayed as defaultType being assignable to constraintType it's really checking if defaultType is assignable to getTypeWithThisArgument(instantiateType(constraintType, makeUnaryTypeMapper(typeParameter, defaultType)), defaultType).

Specifically this is being substituted with the defaultType and the misleading errors about that ensues where it suddenly switches from talking about Document<ElementFieldType> (a part of defaultType) being compared to AnyDocument (the constraintType) to talking comparing Document<ElementFieldType> to the defaultType because this = defaultType. This is confusing because you don't normally expect to see the defaultType being compared to a part of the defaultType.

I admittedly don't understand the rationale for this specific substitution very well but I find it a bit surprising this is substituted with defaultType instead of constratintType. Regardless of what fix would be necessary to make this work, I can't deduce a way to cause unsoundness if this generic parameter default would be accepted.

Contributor guide