Skip to content

⬅️ Back to Table of Contents

📄 isUnsafeAssignment

📊 Analysis Summary

Metric Count
🔧 Functions 2
📦 Imports 4

📚 Table of Contents

🛠️ File Location:

📂 packages/type-utils/src/isUnsafeAssignment.ts

📦 Imports

Name Source
TSESTree @typescript-eslint/utils
AST_NODE_TYPES @typescript-eslint/utils
isTypeAnyType ./predicates
isTypeUnknownType ./predicates

Functions

isUnsafeAssignment(type: ts.Type, receiver: ts.Type, checker: ts.TypeChecker, senderNode: TSESTree.Node | null): false | { receiver: ts.Type; sender: ts.Type }

Does a simple check to see if there is an any being assigned to a non-any type.

This also checks generic positions to ensure there's no unsafe sub-assignments. Note: in the case of generic positions, it makes the assumption that the two types are the same.

Returns: undefined false if it's safe, or an object with the two types if it's unsafe

Examples:

See tests for examples
Raw JSDoc
/**
 * Does a simple check to see if there is an any being assigned to a non-any type.
 *
 * This also checks generic positions to ensure there's no unsafe sub-assignments.
 * Note: in the case of generic positions, it makes the assumption that the two types are the same.
 *
 * @example See tests for examples
 *
 * @returns false if it's safe, or an object with the two types if it's unsafe
 */

Calls:

  • isUnsafeAssignmentWorker
Code
export function isUnsafeAssignment(
  type: ts.Type,
  receiver: ts.Type,
  checker: ts.TypeChecker,
  senderNode: TSESTree.Node | null,
): false | { receiver: ts.Type; sender: ts.Type } {
  return isUnsafeAssignmentWorker(
    type,
    receiver,
    checker,
    senderNode,
    new Map(),
  );
}

isUnsafeAssignmentWorker(…): false | { receiver: ts.Type; sender: ts.Type }

Parameters:

  • type ts.Type
  • receiver ts.Type
  • checker ts.TypeChecker
  • senderNode TSESTree.Node | null
  • visited Map<ts.Type, Set<ts.Type>>

Returns: false | { receiver: ts.Type; sender: ts.Type }

Calls:

  • isTypeAnyType (from ./predicates)
  • isTypeUnknownType (from ./predicates)
  • visited.get
  • typeAlreadyVisited.has
  • typeAlreadyVisited.add
  • visited.set
  • tsutils.isTypeReference
  • isUnsafeAssignmentWorker

Internal Comments:

// Allow assignment of any ==> unknown.
// TODO - figure out how to handle cases like this,
// where the types are assignable, but not the same type
/*
    function foo(): ReadonlySet<number> { return new Set<any>(); }

    // and

    type Test<T> = { prop: T }
    type Test2 = { prop: string }
    declare const a: Test<any>;
    const b: Test2 = a;
    */
// if the type references are different, assume safe, as we won't know how to compare the two types
// the generic positions might not be equivalent for both types
// special case to handle `new Map()`
// unfortunately Map's default empty constructor is typed to return `Map<any, any>` :(
// https://github.com/typescript-eslint/typescript-eslint/issues/2109#issuecomment-634144396

Code
function isUnsafeAssignmentWorker(
  type: ts.Type,
  receiver: ts.Type,
  checker: ts.TypeChecker,
  senderNode: TSESTree.Node | null,
  visited: Map<ts.Type, Set<ts.Type>>,
): false | { receiver: ts.Type; sender: ts.Type } {
  if (isTypeAnyType(type)) {
    // Allow assignment of any ==> unknown.
    if (isTypeUnknownType(receiver)) {
      return false;
    }

    if (!isTypeAnyType(receiver)) {
      return { receiver, sender: type };
    }
  }

  const typeAlreadyVisited = visited.get(type);

  if (typeAlreadyVisited) {
    if (typeAlreadyVisited.has(receiver)) {
      return false;
    }
    typeAlreadyVisited.add(receiver);
  } else {
    visited.set(type, new Set([receiver]));
  }

  if (tsutils.isTypeReference(type) && tsutils.isTypeReference(receiver)) {
    // TODO - figure out how to handle cases like this,
    // where the types are assignable, but not the same type
    /*
    function foo(): ReadonlySet<number> { return new Set<any>(); }

    // and

    type Test<T> = { prop: T }
    type Test2 = { prop: string }
    declare const a: Test<any>;
    const b: Test2 = a;
    */

    if (type.target !== receiver.target) {
      // if the type references are different, assume safe, as we won't know how to compare the two types
      // the generic positions might not be equivalent for both types
      return false;
    }

    if (
      senderNode?.type === AST_NODE_TYPES.NewExpression &&
      senderNode.callee.type === AST_NODE_TYPES.Identifier &&
      senderNode.callee.name === 'Map' &&
      senderNode.arguments.length === 0 &&
      senderNode.typeArguments == null
    ) {
      // special case to handle `new Map()`
      // unfortunately Map's default empty constructor is typed to return `Map<any, any>` :(
      // https://github.com/typescript-eslint/typescript-eslint/issues/2109#issuecomment-634144396
      return false;
    }

    const typeArguments = type.typeArguments ?? [];
    const receiverTypeArguments = receiver.typeArguments ?? [];

    for (let i = 0; i < typeArguments.length; i += 1) {
      const arg = typeArguments[i];
      const receiverArg = receiverTypeArguments[i];

      const unsafe = isUnsafeAssignmentWorker(
        arg,
        receiverArg,
        checker,
        senderNode,
        visited,
      );
      if (unsafe) {
        return { receiver, sender: type };
      }
    }

    return false;
  }

  return false;
}

Generated by Syntax Scribe