Type utilities for working with TypeScript + ESLint together
—
Functions for extracting and analyzing property types from complex type structures. These utilities help understand object properties, their types, and relationships within TypeScript's type system.
Functions for getting property types from complex objects and type structures.
/**
* Gets the type of a property by name, handling symbolic names correctly
*/
function getTypeOfPropertyOfName(
checker: ts.TypeChecker,
type: ts.Type,
name: string,
escapedName?: ts.__String
): ts.Type | undefined;
/**
* Gets the type of a property using a symbol
*/
function getTypeOfPropertyOfType(
checker: ts.TypeChecker,
type: ts.Type,
property: ts.Symbol
): ts.Type | undefined;Usage Examples:
import { getTypeOfPropertyOfName, getTypeOfPropertyOfType } from "@typescript-eslint/type-utils";
// In an ESLint rule analyzing object property access
export default {
create(context) {
const services = context.parserServices;
const checker = services.program.getTypeChecker();
return {
MemberExpression(node) {
if (node.computed === false && node.property.type === "Identifier") {
const objectTsNode = services.esTreeNodeToTSNodeMap.get(node.object);
const objectType = checker.getTypeAtLocation(objectTsNode);
// Get property type by name
const propertyType = getTypeOfPropertyOfName(
checker,
objectType,
node.property.name
);
if (propertyType) {
const propertyTypeName = checker.typeToString(propertyType);
console.log(`Property ${node.property.name} has type: ${propertyTypeName}`);
// Example: Check for specific property types
if (checker.typeToString(propertyType) === "any") {
context.report({
node: node.property,
messageId: "anyPropertyType"
});
}
} else {
context.report({
node: node.property,
messageId: "unknownProperty",
data: { propertyName: node.property.name }
});
}
}
},
TSPropertySignature(node) {
if (node.key.type === "Identifier") {
const tsNode = services.esTreeNodeToTSNodeMap.get(node);
const symbol = checker.getSymbolAtLocation(tsNode);
if (symbol && symbol.parent) {
const parentType = checker.getTypeOfSymbolAtLocation(symbol.parent, tsNode);
// Get property type using symbol
const propertyType = getTypeOfPropertyOfType(checker, parentType, symbol);
if (propertyType) {
console.log(`Property symbol type: ${checker.typeToString(propertyType)}`);
}
}
}
}
};
}
};// Example: Comprehensive object property analysis
import { getTypeOfPropertyOfName, getTypeOfPropertyOfType } from "@typescript-eslint/type-utils";
interface PropertyInfo {
name: string;
type: string;
isOptional: boolean;
isReadonly: boolean;
symbol?: ts.Symbol;
}
function analyzeObjectProperties(
checker: ts.TypeChecker,
objectType: ts.Type
): PropertyInfo[] {
const properties: PropertyInfo[] = [];
const propertySymbols = checker.getPropertiesOfType(objectType);
propertySymbols.forEach(symbol => {
const propertyType = getTypeOfPropertyOfType(checker, objectType, symbol);
if (propertyType) {
const isOptional = (symbol.flags & ts.SymbolFlags.Optional) !== 0;
const isReadonly = symbol.valueDeclaration ?
ts.getCombinedModifierFlags(symbol.valueDeclaration) & ts.ModifierFlags.Readonly : false;
properties.push({
name: symbol.name,
type: checker.typeToString(propertyType),
isOptional,
isReadonly: !!isReadonly,
symbol
});
}
});
return properties;
}
// Usage in ESLint rule
function validateObjectStructure(
services: ParserServicesWithTypeInformation,
objectLiteral: TSESTree.ObjectExpression
) {
const checker = services.program.getTypeChecker();
const tsNode = services.esTreeNodeToTSNodeMap.get(objectLiteral);
const objectType = checker.getTypeAtLocation(tsNode);
const properties = analyzeObjectProperties(checker, objectType);
properties.forEach(prop => {
if (prop.type === "any") {
console.log(`Property ${prop.name} has any type`);
}
if (!prop.isOptional && prop.type.includes("undefined")) {
console.log(`Required property ${prop.name} includes undefined`);
}
});
}// Example: Analyzing dynamic property access patterns
import { getTypeOfPropertyOfName } from "@typescript-eslint/type-utils";
function analyzeDynamicPropertyAccess(
services: ParserServicesWithTypeInformation,
memberExpression: TSESTree.MemberExpression
): {
isValidAccess: boolean;
propertyType: string | null;
suggestions: string[];
} {
const checker = services.program.getTypeChecker();
const objectTsNode = services.esTreeNodeToTSNodeMap.get(memberExpression.object);
const objectType = checker.getTypeAtLocation(objectTsNode);
let propertyName: string;
let isValidAccess = false;
let propertyType: string | null = null;
const suggestions: string[] = [];
if (memberExpression.computed && memberExpression.property.type === "Literal") {
// obj["propertyName"]
propertyName = String(memberExpression.property.value);
} else if (!memberExpression.computed && memberExpression.property.type === "Identifier") {
// obj.propertyName
propertyName = memberExpression.property.name;
} else {
return { isValidAccess: false, propertyType: null, suggestions: [] };
}
// Check if property exists
const type = getTypeOfPropertyOfName(checker, objectType, propertyName);
if (type) {
isValidAccess = true;
propertyType = checker.typeToString(type);
} else {
// Property doesn't exist, suggest similar properties
const allProperties = checker.getPropertiesOfType(objectType);
allProperties.forEach(symbol => {
const similarity = calculateSimilarity(propertyName, symbol.name);
if (similarity > 0.6) { // Basic similarity threshold
suggestions.push(symbol.name);
}
});
}
return { isValidAccess, propertyType, suggestions };
}
function calculateSimilarity(a: string, b: string): number {
// Simple similarity calculation (Levenshtein-based)
const longer = a.length > b.length ? a : b;
const shorter = a.length > b.length ? b : a;
if (longer.length === 0) return 1.0;
const distance = levenshteinDistance(longer, shorter);
return (longer.length - distance) / longer.length;
}
function levenshteinDistance(str1: string, str2: string): number {
const matrix = Array(str2.length + 1).fill(null).map(() => Array(str1.length + 1).fill(null));
for (let i = 0; i <= str1.length; i++) matrix[0][i] = i;
for (let j = 0; j <= str2.length; j++) matrix[j][0] = j;
for (let j = 1; j <= str2.length; j++) {
for (let i = 1; i <= str1.length; i++) {
const indicator = str1[i - 1] === str2[j - 1] ? 0 : 1;
matrix[j][i] = Math.min(
matrix[j][i - 1] + 1,
matrix[j - 1][i] + 1,
matrix[j - 1][i - 1] + indicator
);
}
}
return matrix[str2.length][str1.length];
}// Example: Deep property type analysis for nested objects
import { getTypeOfPropertyOfName } from "@typescript-eslint/type-utils";
interface NestedPropertyPath {
path: string[];
type: string;
depth: number;
}
function analyzeNestedProperties(
checker: ts.TypeChecker,
rootType: ts.Type,
maxDepth: number = 3
): NestedPropertyPath[] {
const paths: NestedPropertyPath[] = [];
function traverse(type: ts.Type, currentPath: string[], depth: number) {
if (depth >= maxDepth) return;
const properties = checker.getPropertiesOfType(type);
properties.forEach(symbol => {
const propertyType = getTypeOfPropertyOfName(checker, type, symbol.name);
if (propertyType) {
const newPath = [...currentPath, symbol.name];
const typeString = checker.typeToString(propertyType);
paths.push({
path: newPath,
type: typeString,
depth: depth + 1
});
// Recursively analyze object properties
const propertySymbols = checker.getPropertiesOfType(propertyType);
if (propertySymbols.length > 0 && depth < maxDepth - 1) {
traverse(propertyType, newPath, depth + 1);
}
}
});
}
traverse(rootType, [], 0);
return paths;
}
// Usage example
function validateNestedObjectAccess(
services: ParserServicesWithTypeInformation,
node: TSESTree.MemberExpression
) {
const checker = services.program.getTypeChecker();
// Build property access path
const path: string[] = [];
let current: TSESTree.Node = node;
while (current.type === "MemberExpression") {
if (!current.computed && current.property.type === "Identifier") {
path.unshift(current.property.name);
}
current = current.object;
}
// Get root object type
const rootTsNode = services.esTreeNodeToTSNodeMap.get(current);
const rootType = checker.getTypeAtLocation(rootTsNode);
// Validate each step in the path
let currentType = rootType;
for (const propertyName of path) {
const propertyType = getTypeOfPropertyOfName(checker, currentType, propertyName);
if (!propertyType) {
console.log(`Property ${propertyName} not found in path ${path.join('.')}`);
break;
}
currentType = propertyType;
}
}// Example: Analyzing index signatures and dynamic properties
import { getTypeOfPropertyOfName } from "@typescript-eslint/type-utils";
function analyzeIndexSignatures(
checker: ts.TypeChecker,
type: ts.Type
): {
hasStringIndex: boolean;
hasNumberIndex: boolean;
stringIndexType?: string;
numberIndexType?: string;
dynamicPropertyAccess: boolean;
} {
const stringIndexType = checker.getIndexTypeOfType(type, ts.IndexKind.String);
const numberIndexType = checker.getIndexTypeOfType(type, ts.IndexKind.Number);
return {
hasStringIndex: !!stringIndexType,
hasNumberIndex: !!numberIndexType,
stringIndexType: stringIndexType ? checker.typeToString(stringIndexType) : undefined,
numberIndexType: numberIndexType ? checker.typeToString(numberIndexType) : undefined,
dynamicPropertyAccess: !!(stringIndexType || numberIndexType)
};
}
// Check if dynamic property access is safe
function validateDynamicAccess(
services: ParserServicesWithTypeInformation,
memberExpression: TSESTree.MemberExpression
) {
if (!memberExpression.computed) return;
const checker = services.program.getTypeChecker();
const objectTsNode = services.esTreeNodeToTSNodeMap.get(memberExpression.object);
const objectType = checker.getTypeAtLocation(objectTsNode);
const indexInfo = analyzeIndexSignatures(checker, objectType);
if (!indexInfo.dynamicPropertyAccess) {
console.log("Object doesn't support dynamic property access");
return;
}
// Validate property key type
const propertyTsNode = services.esTreeNodeToTSNodeMap.get(memberExpression.property);
const propertyType = checker.getTypeAtLocation(propertyTsNode);
const propertyTypeString = checker.typeToString(propertyType);
if (indexInfo.hasStringIndex && propertyTypeString !== "string") {
console.log("String index signature requires string key");
}
if (indexInfo.hasNumberIndex && propertyTypeString !== "number") {
console.log("Number index signature requires number key");
}
}Install with Tessl CLI
npx tessl i tessl/npm-typescript-eslint--type-utils