CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-typescript-eslint--type-utils

Type utilities for working with TypeScript + ESLint together

Pending
Overview
Eval results
Files

type-safety.mddocs/

Type Safety Utilities

Functions for detecting potentially unsafe type operations, particularly around readonly properties and any type assignments. These utilities help identify code patterns that may lead to runtime errors or violate type safety guarantees.

Capabilities

Readonly Type Analysis

Functions for analyzing whether types and their properties are readonly, crucial for understanding mutability guarantees.

interface ReadonlynessOptions {
  readonly allow?: TypeOrValueSpecifier[];
  readonly treatMethodsAsReadonly?: boolean;
}

/**
 * Checks if the given type is readonly. Handles arrays, tuples, objects, 
 * properties, index signatures, unions, intersections, and conditional types.
 */
function isTypeReadonly(
  program: ts.Program, 
  type: ts.Type, 
  options?: ReadonlynessOptions
): boolean;

Constants:

const readonlynessOptionsDefaults: ReadonlynessOptions;
const readonlynessOptionsSchema: JSONSchema4;

Usage Examples:

import { isTypeReadonly, readonlynessOptionsDefaults } from "@typescript-eslint/type-utils";

// In an ESLint rule checking for readonly violations
export default {
  create(context) {
    const services = context.parserServices;
    const program = services.program;

    return {
      AssignmentExpression(node) {
        if (node.left.type === "MemberExpression") {
          const tsNode = services.esTreeNodeToTSNodeMap.get(node.left.object);
          const type = program.getTypeChecker().getTypeAtLocation(tsNode);
          
          if (isTypeReadonly(program, type)) {
            context.report({
              node,
              messageId: "readonlyViolation"
            });
          }
        }
      },
      
      CallExpression(node) {
        // Check method calls on readonly types
        if (node.callee.type === "MemberExpression") {
          const tsObject = services.esTreeNodeToTSNodeMap.get(node.callee.object);
          const objectType = program.getTypeChecker().getTypeAtLocation(tsObject);
          
          const options = {
            ...readonlynessOptionsDefaults,
            treatMethodsAsReadonly: true
          };
          
          if (isTypeReadonly(program, objectType, options)) {
            // Check if this method mutates the object
            const methodName = node.callee.property.name;
            if (['push', 'pop', 'shift', 'unshift', 'splice'].includes(methodName)) {
              context.report({
                node,
                messageId: "mutatingMethodOnReadonly"
              });
            }
          }
        }
      }
    };
  }
};

Unsafe Assignment Detection

Functions for detecting unsafe assignments, particularly those involving any types that could bypass TypeScript's type checking.

/**
 * Checks if there's an unsafe assignment of `any` to a non-`any` type. 
 * Also checks generic positions for unsafe sub-assignments.
 * @returns false if safe, or an object with the two types if unsafe
 */
function isUnsafeAssignment(
  type: ts.Type, 
  receiver: ts.Type, 
  checker: ts.TypeChecker, 
  senderNode: TSESTree.Node | null
): false | { receiver: ts.Type; sender: ts.Type };

Usage Examples:

import { isUnsafeAssignment } from "@typescript-eslint/type-utils";

// In an ESLint rule detecting unsafe assignments
export default {
  create(context) {
    const services = context.parserServices;
    const checker = services.program.getTypeChecker();

    return {
      AssignmentExpression(node) {
        const leftTsNode = services.esTreeNodeToTSNodeMap.get(node.left);
        const rightTsNode = services.esTreeNodeToTSNodeMap.get(node.right);
        
        const leftType = checker.getTypeAtLocation(leftTsNode);
        const rightType = checker.getTypeAtLocation(rightTsNode);
        
        const unsafeAssignment = isUnsafeAssignment(rightType, leftType, checker, node.right);
        
        if (unsafeAssignment) {
          context.report({
            node,
            messageId: "unsafeAnyAssignment",
            data: {
              sender: checker.typeToString(unsafeAssignment.sender),
              receiver: checker.typeToString(unsafeAssignment.receiver)
            }
          });
        }
      },
      
      VariableDeclarator(node) {
        if (node.init && node.id.typeAnnotation) {
          const initTsNode = services.esTreeNodeToTSNodeMap.get(node.init);
          const idTsNode = services.esTreeNodeToTSNodeMap.get(node.id);
          
          const initType = checker.getTypeAtLocation(initTsNode);
          const declaredType = checker.getTypeAtLocation(idTsNode);
          
          const unsafeAssignment = isUnsafeAssignment(initType, declaredType, checker, node.init);
          
          if (unsafeAssignment) {
            context.report({
              node: node.init,
              messageId: "unsafeAnyInitialization"
            });
          }
        }
      }
    };
  }
};

Advanced Safety Analysis Patterns

Readonly Options Configuration

// Example: Configuring readonly analysis with custom options
import { isTypeReadonly, TypeOrValueSpecifier } from "@typescript-eslint/type-utils";

const customReadonlyOptions = {
  allow: [
    // Allow specific types to be treated as non-readonly
    { from: 'lib', name: 'Array' },
    { from: 'package', name: 'MutableArray', package: 'custom-utils' }
  ] as TypeOrValueSpecifier[],
  treatMethodsAsReadonly: false
};

function checkCustomReadonly(program: ts.Program, type: ts.Type): boolean {
  return isTypeReadonly(program, type, customReadonlyOptions);
}

Generic Unsafe Assignment Detection

// Example: Detecting unsafe assignments in generic contexts
import { isUnsafeAssignment } from "@typescript-eslint/type-utils";

function analyzeGenericAssignment(
  services: ParserServicesWithTypeInformation,
  node: TSESTree.CallExpression
) {
  const checker = services.program.getTypeChecker();
  
  // Check each argument for unsafe assignments
  node.arguments.forEach((arg, index) => {
    const argTsNode = services.esTreeNodeToTSNodeMap.get(arg);
    const argType = checker.getTypeAtLocation(argTsNode);
    
    // Get the expected parameter type
    const callTsNode = services.esTreeNodeToTSNodeMap.get(node);
    const signature = checker.getResolvedSignature(callTsNode);
    
    if (signature && signature.parameters[index]) {
      const paramType = checker.getTypeOfSymbolAtLocation(
        signature.parameters[index], 
        callTsNode
      );
      
      const unsafeAssignment = isUnsafeAssignment(argType, paramType, checker, arg);
      
      if (unsafeAssignment) {
        console.log(`Unsafe assignment in argument ${index}`);
      }
    }
  });
}

Complex Readonly Analysis

// Example: Deep readonly analysis for nested structures
import { isTypeReadonly } from "@typescript-eslint/type-utils";

function analyzeNestedReadonly(
  program: ts.Program, 
  type: ts.Type,
  checker: ts.TypeChecker
): { isReadonly: boolean; issues: string[] } {
  const issues: string[] = [];
  
  // Check top-level readonly
  const isTopLevelReadonly = isTypeReadonly(program, type);
  
  if (!isTopLevelReadonly) {
    issues.push("Top-level type is not readonly");
  }
  
  // Check properties for nested structures
  const properties = checker.getPropertiesOfType(type);
  
  properties.forEach(prop => {
    const propType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration!);
    
    if (!isTypeReadonly(program, propType)) {
      issues.push(`Property '${prop.name}' is not readonly`);
    }
  });
  
  return {
    isReadonly: isTopLevelReadonly && issues.length === 1, // Only top-level issue
    issues
  };
}

Safe Assignment Validation

// Example: Comprehensive assignment safety checking
import { isUnsafeAssignment, isTypeReadonly } from "@typescript-eslint/type-utils";

function validateAssignmentSafety(
  services: ParserServicesWithTypeInformation,
  assignment: TSESTree.AssignmentExpression
): { safe: boolean; issues: string[] } {
  const checker = services.program.getTypeChecker();
  const program = services.program;
  const issues: string[] = [];
  
  const leftTsNode = services.esTreeNodeToTSNodeMap.get(assignment.left);
  const rightTsNode = services.esTreeNodeToTSNodeMap.get(assignment.right);
  
  const leftType = checker.getTypeAtLocation(leftTsNode);
  const rightType = checker.getTypeAtLocation(rightTsNode);
  
  // Check for unsafe any assignments
  const unsafeAssignment = isUnsafeAssignment(rightType, leftType, checker, assignment.right);
  if (unsafeAssignment) {
    issues.push("Unsafe any assignment detected");
  }
  
  // Check readonly violations
  if (assignment.left.type === "MemberExpression") {
    const objectTsNode = services.esTreeNodeToTSNodeMap.get(assignment.left.object);
    const objectType = checker.getTypeAtLocation(objectTsNode);
    
    if (isTypeReadonly(program, objectType)) {
      issues.push("Assignment to readonly property");
    }
  }
  
  return {
    safe: issues.length === 0,
    issues
  };
}

Install with Tessl CLI

npx tessl i tessl/npm-typescript-eslint--type-utils

docs

builtin-types.md

index.md

property-types.md

symbol-analysis.md

type-analysis.md

type-constraints.md

type-predicates.md

type-safety.md

type-specifiers.md

tile.json