Skip to content

Repo: Investigate better handling of unconstrained generics #10438

@kirkwaiblinger

Description

@kirkwaiblinger

Suggestion

Sadly, right now getConstrainedTypeOfLocation is bugged for unconstrained generics. The code just gives back the type parameter type if getBaseConstraintOfType(nodeType) is nullish, but that's not correct. Unconstrained generics should behave like unknown. Here's what it is right now:

/**
* Resolves the given node's type. Will resolve to the type's generic constraint, if it has one.
*/
export function getConstrainedTypeAtLocation(
services: ParserServicesWithTypeInformation,
node: TSESTree.Node,
): ts.Type {
const nodeType = services.getTypeAtLocation(node);
const constrained = services.program
.getTypeChecker()
.getBaseConstraintOfType(nodeType);
return constrained ?? nodeType;
}

And here's what it "should" be:

/**
 * Resolves the given node's type. Will resolve to the type's generic constraint, if it has one.
 */
export function getConstrainedTypeAtLocation(
  services: ParserServicesWithTypeInformation,
  node: TSESTree.Node,
): ts.Type {
  const nodeType = services.getTypeAtLocation(node);
  if (tsutils.isTypeParameter(nodeType)) {
    const checker = services.program.getTypeChecker();

    return (
      checker.getBaseConstraintOfType(nodeType) ?? checker.getUnknownType()
    );
  }
  return nodeType;
}

Unfortunately, due to microsoft/TypeScript#60475, there is no checker.getUnknownType() yet or for a while.

It kind of sucks, but I propose that we instead modify the implementation to something more like

export function getConstrainedTypeAtLocation(
  services: ParserServicesWithTypeInformation,
  node: TSESTree.Node,
): ts.Type | undefined { // <-- note the `| undefined`
  const nodeType = services.getTypeAtLocation(node);
  if (tsutils.isTypeParameter(nodeType)) {
    const checker = services.program.getTypeChecker();
    return checker.getBaseConstraintOfType(nodeType) 
    /* in the future this can have ?? checker.getUnknownType() */;
  }
  return nodeType;
}

and force callers to handle the unconstrained case separately.


Note - I think we should also double-check (and add tests) for what happens when the constraint is any. I'm pretty sure function f<T extends any>(x: T) {} behaves as though T were unknown rather than any. So maybe we should really be doing

export function getConstrainedTypeAtLocation(
  services: ParserServicesWithTypeInformation,
  node: TSESTree.Node,
): ts.Type | undefined {
  const nodeType = services.getTypeAtLocation(node);
  if (tsutils.isTypeParameter(nodeType)) {
    const checker = services.program.getTypeChecker();
    const constraint = checker.getBaseConstraintOfType(nodeType);
    return constraint == null || isTypeAnyType(constraint)
      ? /* in the future this can be checker.getUnknownType() */ undefined
      : constraint;
  }
  return nodeType;
}

I haven't yet checked what checker.getBaseConstraintOfType() does with an any.

Additional Info

Related, caused #10311
Related, discussion at #10314 (comment)
Possibly related, #10415

cc @ronami

Metadata

Metadata

Assignees

No one assigned

    Labels

    accepting prsGo ahead, send a pull request that resolves this issuelocked due to agePlease open a new issue if you'd like to say more. See https://typescript-eslint.io/contributing.repo maintenancethings to do with maintenance of the repo, and not with code/docs

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions