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-specifiers.mddocs/

Type Specifiers

Advanced type matching system using specifiers to identify types from specific packages, files, or the TypeScript standard library. This system provides flexible type identification beyond simple name matching.

Capabilities

Type Specifier Definitions

Core types and interfaces for the type specifier system.

interface FileSpecifier {
  from: 'file';
  name: string | string[];
  path?: string;
}

interface LibSpecifier {
  from: 'lib';
  name: string | string[];
}

interface PackageSpecifier {
  from: 'package';
  name: string | string[];
  package: string;
}

type TypeOrValueSpecifier = string | FileSpecifier | LibSpecifier | PackageSpecifier;

Constants:

const typeOrValueSpecifiersSchema: JSONSchema4;

Type Matching Functions

Functions for matching types against specifiers.

/**
 * Checks if a type matches a specific TypeOrValueSpecifier
 */
function typeMatchesSpecifier(
  type: ts.Type, 
  specifier: TypeOrValueSpecifier, 
  program: ts.Program
): boolean;

/**
 * Checks if a type matches any of the provided specifiers
 */
function typeMatchesSomeSpecifier(
  type: ts.Type, 
  specifiers: TypeOrValueSpecifier[], 
  program: ts.Program
): boolean;

Value Matching Functions

Functions for matching ESTree nodes against specifiers.

/**
 * Checks if a value node matches a specific TypeOrValueSpecifier
 */
function valueMatchesSpecifier(
  node: TSESTree.Node, 
  specifier: TypeOrValueSpecifier, 
  program: ts.Program, 
  type: ts.Type
): boolean;

/**
 * Checks if a value node matches any of the provided specifiers
 */
function valueMatchesSomeSpecifier(
  node: TSESTree.Node, 
  specifiers: TypeOrValueSpecifier[], 
  program: ts.Program, 
  type: ts.Type
): boolean;

Usage Examples

Basic Type Specifier Usage

import { 
  typeMatchesSpecifier, 
  typeMatchesSomeSpecifier,
  TypeOrValueSpecifier 
} from "@typescript-eslint/type-utils";

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

    // Define allowed types using specifiers
    const allowedTypes: TypeOrValueSpecifier[] = [
      // Simple string name
      "string",
      
      // Built-in lib types
      { from: "lib", name: "Promise" },
      { from: "lib", name: ["Array", "ReadonlyArray"] },
      
      // Package types
      { from: "package", name: "Observable", package: "rxjs" },
      { from: "package", name: ["Component", "Injectable"], package: "@angular/core" },
      
      // File-specific types
      { from: "file", name: "UserType", path: "./types/user.ts" },
      { from: "file", name: "ApiResponse" } // Any file
    ];

    return {
      VariableDeclarator(node) {
        if (node.init) {
          const tsNode = services.esTreeNodeToTSNodeMap.get(node.init);
          const type = checker.getTypeAtLocation(tsNode);
          
          // Check if type matches any allowed specifier
          if (!typeMatchesSomeSpecifier(type, allowedTypes, program)) {
            context.report({
              node: node.init,
              messageId: "disallowedType",
              data: { 
                typeName: checker.typeToString(type) 
              }
            });
          }
        }
      }
    };
  }
};

Package-Specific Type Checking

import { typeMatchesSpecifier, PackageSpecifier } from "@typescript-eslint/type-utils";

// Check for specific React types
const reactSpecifiers: PackageSpecifier[] = [
  { from: "package", name: "Component", package: "react" },
  { from: "package", name: "FC", package: "react" },
  { from: "package", name: "ReactNode", package: "react" }
];

export default {
  create(context) {
    const services = context.parserServices;
    const program = services.program;
    const checker = program.getTypeChecker();

    return {
      ClassDeclaration(node) {
        if (node.superClass) {
          const tsNode = services.esTreeNodeToTSNodeMap.get(node.superClass);
          const type = checker.getTypeAtLocation(tsNode);
          
          const isReactComponent = reactSpecifiers.some(spec => 
            typeMatchesSpecifier(type, spec, program)
          );
          
          if (isReactComponent) {
            // This class extends a React component
            console.log("React component detected");
          }
        }
      }
    };
  }
};

File-Based Type Restrictions

import { typeMatchesSpecifier, FileSpecifier } from "@typescript-eslint/type-utils";

// Restrict types to specific files
const internalTypeSpecifiers: FileSpecifier[] = [
  { from: "file", name: ["InternalAPI", "PrivateType"], path: "./internal" },
  { from: "file", name: "ConfigType", path: "./config/types.ts" }
];

export default {
  create(context) {
    const services = context.parserServices;
    const program = services.program;
    const checker = program.getTypeChecker();

    return {
      TSTypeReference(node) {
        const tsNode = services.esTreeNodeToTSNodeMap.get(node);
        const type = checker.getTypeAtLocation(tsNode);
        
        const isInternalType = internalTypeSpecifiers.some(spec =>
          typeMatchesSpecifier(type, spec, program)
        );
        
        if (isInternalType) {
          // Check if this usage is in an appropriate location
          const sourceFile = node.getSourceFile?.() || context.getSourceCode().ast;
          const fileName = sourceFile.filename || "";
          
          if (!fileName.includes("internal")) {
            context.report({
              node,
              messageId: "internalTypeInPublicAPI"
            });
          }
        }
      }
    };
  }
};

Value Specifier Usage

import { 
  valueMatchesSpecifier, 
  valueMatchesSomeSpecifier,
  TypeOrValueSpecifier 
} from "@typescript-eslint/type-utils";

// Check values (not just types)
const dangerousFunctions: TypeOrValueSpecifier[] = [
  { from: "lib", name: "eval" },
  { from: "package", name: "exec", package: "child_process" },
  { from: "file", name: "unsafeOperation", path: "./unsafe-utils.ts" }
];

export default {
  create(context) {
    const services = context.parserServices;
    const program = services.program;
    const checker = program.getTypeChecker();

    return {
      CallExpression(node) {
        const tsNode = services.esTreeNodeToTSNodeMap.get(node.callee);
        const type = checker.getTypeAtLocation(tsNode);
        
        if (valueMatchesSomeSpecifier(node.callee, dangerousFunctions, program, type)) {
          context.report({
            node: node.callee,
            messageId: "dangerousFunctionCall"
          });
        }
      }
    };
  }
};

Advanced Specifier Patterns

Dynamic Specifier Generation

// Example: Creating specifiers based on configuration
import { TypeOrValueSpecifier } from "@typescript-eslint/type-utils";

interface AllowedTypeConfig {
  packageName: string;
  allowedTypes: string[];
  allowedFiles?: string[];
}

function createSpecifiersFromConfig(configs: AllowedTypeConfig[]): TypeOrValueSpecifier[] {
  const specifiers: TypeOrValueSpecifier[] = [];
  
  configs.forEach(config => {
    // Add package specifiers
    config.allowedTypes.forEach(typeName => {
      specifiers.push({
        from: "package",
        name: typeName,
        package: config.packageName
      });
    });
    
    // Add file specifiers if specified
    config.allowedFiles?.forEach(filePath => {
      config.allowedTypes.forEach(typeName => {
        specifiers.push({
          from: "file",
          name: typeName,
          path: filePath
        });
      });
    });
  });
  
  return specifiers;
}

// Usage
const typeConfigs: AllowedTypeConfig[] = [
  {
    packageName: "lodash",
    allowedTypes: ["Dictionary", "List"],
    allowedFiles: ["./types/lodash.d.ts"]
  },
  {
    packageName: "rxjs",
    allowedTypes: ["Observable", "Subject", "BehaviorSubject"]
  }
];

const allowedSpecifiers = createSpecifiersFromConfig(typeConfigs);

Conditional Type Matching

// Example: Conditional type matching based on context
import { typeMatchesSpecifier, TypeOrValueSpecifier } from "@typescript-eslint/type-utils";

function createContextualTypeChecker(
  baseSpecifiers: TypeOrValueSpecifier[],
  contextRules: Map<string, TypeOrValueSpecifier[]>
) {
  return function checkTypeInContext(
    type: ts.Type,
    program: ts.Program,
    context: string
  ): boolean {
    // Check base allowed types first
    const baseAllowed = baseSpecifiers.some(spec => 
      typeMatchesSpecifier(type, spec, program)
    );
    
    if (baseAllowed) return true;
    
    // Check context-specific rules
    const contextSpecifiers = contextRules.get(context);
    if (contextSpecifiers) {
      return contextSpecifiers.some(spec => 
        typeMatchesSpecifier(type, spec, program)
      );
    }
    
    return false;
  };
}

// Usage
const baseTypes: TypeOrValueSpecifier[] = [
  "string", "number", "boolean",
  { from: "lib", name: "Promise" }
];

const contextRules = new Map([
  ["test", [
    { from: "package", name: "jest", package: "@types/jest" },
    { from: "package", name: "TestingLibrary", package: "@testing-library/react" }
  ]],
  ["api", [
    { from: "package", name: "Request", package: "express" },
    { from: "package", name: "Response", package: "express" }
  ]]
]);

const typeChecker = createContextualTypeChecker(baseTypes, contextRules);

Hierarchical Type Checking

// Example: Hierarchical type matching with inheritance
import { typeMatchesSpecifier, LibSpecifier } from "@typescript-eslint/type-utils";

interface TypeHierarchy {
  base: TypeOrValueSpecifier[];
  derived: Map<string, TypeOrValueSpecifier[]>;
}

function checkTypeHierarchy(
  type: ts.Type,
  program: ts.Program,
  hierarchy: TypeHierarchy,
  checker: ts.TypeChecker
): { matches: boolean; level: string } {
  // Check base level first
  const baseMatches = hierarchy.base.some(spec => 
    typeMatchesSpecifier(type, spec, program)
  );
  
  if (baseMatches) {
    return { matches: true, level: "base" };
  }
  
  // Check derived levels
  for (const [level, specifiers] of hierarchy.derived) {
    const derivedMatches = specifiers.some(spec => 
      typeMatchesSpecifier(type, spec, program)
    );
    
    if (derivedMatches) {
      return { matches: true, level };
    }
  }
  
  return { matches: false, level: "none" };
}

// Usage for React component hierarchy
const reactHierarchy: TypeHierarchy = {
  base: [
    { from: "lib", name: "HTMLElement" },
    { from: "package", name: "ReactNode", package: "react" }
  ],
  derived: new Map([
    ["component", [
      { from: "package", name: "Component", package: "react" },
      { from: "package", name: "PureComponent", package: "react" }
    ]],
    ["functional", [
      { from: "package", name: "FC", package: "react" },
      { from: "package", name: "FunctionComponent", package: "react" }
    ]]
  ])
};

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