Type utilities for working with TypeScript + ESLint together
—
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.
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;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;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;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)
}
});
}
}
}
};
}
};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");
}
}
}
};
}
};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"
});
}
}
}
};
}
};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"
});
}
}
};
}
};// 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);// 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);// 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