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