
Company News
Socket Partners with Replit to Block Malicious Packages in AI-Powered Development
Replit is integrating Socket Firewall into its AI-powered development experience to help protect builders from malicious open source packages.
@shaenchen/custom-type-enforcement
Advanced tools
Enforce TypeScript type architecture and code quality rules
Enforce TypeScript type architecture and code quality rules across any TypeScript project
A portable CLI tool that enforces custom TypeScript type architecture patterns and prevents common code quality issues. Works seamlessly with single packages and monorepos.
This tool enforces opinionated preferences for TypeScript architecture. Not everyone will agree with these rules, and that's okay. These patterns emerged from real-world experience maintaining large TypeScript codebases where certain architectural decisions proved their worth over time.
While you might initially wonder "what value does this bring?", these checks address common pain points:
🔍 Findability: When types live in predictable locations (types.ts files), developers know exactly where to look. No more hunting through implementation files or following circular imports.
♻️ Maintainability: Named types (vs inline types) are searchable, refactorable, and provide better error messages. Changes propagate cleanly through the codebase.
🎯 Consistency: Enforcing patterns across teams prevents the "every file is different" problem that makes codebases hard to navigate as they scale.
📦 Modularity: Preventing barrel files and enforcing type organization reduces circular dependencies and improves tree-shaking.
You don't need to adopt all checks. Use --checks to run only what makes sense for your project:
--checks=type-exports,type-imports--checks=type-duplicatesbarrel-files checkThese rules work best for medium-to-large codebases where consistency and maintainability matter more than individual file convenience. For smaller projects or prototypes, some rules might feel like overkill—and that's a valid perspective.
npm install -D @shaenchen/custom-type-enforcement
Or use directly with npx:
npx @shaenchen/custom-type-enforcement
Run all checks in your TypeScript project:
npx custom-type-enforcement
Run specific checks only:
npx custom-type-enforcement --checks=barrel-files,type-exports
Add to your package.json scripts:
{
"scripts": {
"check": "custom-type-enforcement",
"check:types": "custom-type-enforcement --checks=type-exports,type-imports"
}
}
Purpose: Prevent pure wrapper files that only re-export from other files.
Detects: Files that ONLY contain:
export { ... } from '...'export * from '...'export type { ... } from '...'Rationale:
Example violation:
// ❌ BAD: Pure barrel file (utils/index.ts)
export { formatDate } from './date'
export { parseUrl } from './url'
export * from './helpers'
// ✅ GOOD: Add actual implementation
export { formatDate } from './date'
export { parseUrl } from './url'
// Utility function in this file
export function compose(...fns) {
return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
}
Allow barrel files when needed:
// @barrel-file-allowed
export * from './components'
Purpose: Enforce that types/interfaces/enums are only exported from types.ts or types/{domain}.ts files.
Rules:
✅ Allowed:
types.ts filestypes/{domain}.ts files (can export anything: types, functions, constants, classes)new expressions, objects with runtime values)❌ Forbidden:
export type * pattern (discouraged everywhere, including in types/ directories)Example violations:
// ❌ BAD: Exporting types from implementation file (user.service.ts)
export interface User {
id: string
name: string
}
export class UserService {
// ...
}
// ✅ GOOD: Move types to types.ts
// types.ts
export interface User {
id: string
name: string
}
// user.service.ts
import type { User } from './types'
export class UserService {
// ...
}
File path validation: Types must be in files ending with types.ts or containing /types/ in the path.
Types/ directories allow anything: Files in types/ directories can export anything—types, type guards, helper functions, constants, and classes. This pragmatic approach recognizes that:
as const arrays are often used with Zod schemas// ✅ GOOD: types/alerts.ts - all exports allowed
export interface ThresholdCondition {
threshold: number;
operator: 'gt' | 'lt' | 'eq';
}
export interface RuleCondition {
rules: string[];
}
export type AlertCondition = ThresholdCondition | RuleCondition;
// Type guards - allowed in types/ directory
export function isThresholdCondition(c: AlertCondition): c is ThresholdCondition {
return 'threshold' in c;
}
// Constants - allowed in types/ directory
export const ALERT_TYPES = ['threshold', 'rule'] as const;
export const DEFAULT_TIMEOUT = 5000;
Runtime constants are automatically allowed:
// ✅ GOOD: These are runtime values, not type definitions
// Function call initializers
export const logger = pino({ level: 'info' });
export const client = axios.create({ baseURL: 'https://api.example.com' });
// new expressions
export const service = new MyService();
export const pool = new Pool({ connectionString: DB_URL });
// Object literals with runtime values (identifiers, property access, function calls)
export const config = {
url: process.env.DATABASE_URL,
timeout: getDefaultTimeout(),
...baseConfig,
};
// TypeBox/Zod schemas (runtime validators)
export const UserSchema = Type.Object({ name: Type.String() });
export const EmailSchema = z.string().email();
// ❌ BAD: These are type-like constants (pure literals)
export const API_KEY = 'my-api-key'; // Primitive literal
export const MAX_RETRIES = 3; // Primitive literal
export const config = { timeout: 5000 }; // Object with only literals
export const items = ['one', 'two']; // Array of literals
Allow type exports when needed:
// @type-export-allowed
export interface LegacyType {
// Special case that needs to be exported from this file
}
Purpose: Enforce that type imports only come from types.ts or types/{domain}.ts files.
Rules:
✅ Allowed:
types.ts filestypes/{domain}.ts files❌ Forbidden:
Example violations:
// ❌ BAD: Importing type from implementation file
import type { User } from './user.service'
// ✅ GOOD: Import from types file
import type { User } from './types'
// ✅ GOOD: Import from external package
import type { Request, Response } from 'express'
Mixed imports:
// Both syntax variants are detected:
import type { User, Product } from './services' // ❌ BAD
import { type User, UserService } from './services' // ❌ BAD (type from non-types file)
import type { User } from './types' // ✅ GOOD
import { UserService } from './services' // ✅ GOOD
Sibling types file imports are automatically allowed:
// ✅ GOOD: types/charts.ts importing from types/metrics.ts
// Both files are in the types/ directory - this is allowed
import type { Metric, MetricValue } from './metrics';
import type { ValidationError } from './common';
export interface ChartData {
metric: Metric;
values: MetricValue[];
}
Allow type imports when needed:
// @type-import-allowed
import type { SpecialType } from './external-library-wrapper'
Purpose: Warn about structurally similar types that could be consolidated.
Detects:
Pick/Omit)Required<T> opportunities (same fields, different optionality)Type1 & Type2)Example violations:
// ❌ BAD: Duplicate types
// types/user.ts
export interface User {
id: string
name: string
email: string
}
// types/customer.ts
export interface Customer {
id: string
name: string
email: string
}
// ✅ GOOD: Use composition
// types/user.ts
export interface BaseUser {
id: string
name: string
email: string
}
export type User = BaseUser
export type Customer = BaseUser
// Or even better, just use one type:
export interface User {
id: string
name: string
email: string
}
Composition opportunities:
// ❌ BAD: Subset duplication
export interface User {
id: string
name: string
email: string
role: string
}
export interface PublicUser {
id: string
name: string
}
// ✅ GOOD: Use Pick
export interface User {
id: string
name: string
email: string
role: string
}
export type PublicUser = Pick<User, 'id' | 'name'>
Ignore duplicates:
// @type-duplicate-allowed
export interface SpecialCaseType {
// Intentionally duplicated for specific reason
}
Smart filtering:
types.ts filesPurpose: Discourage inline object types in favor of named interfaces/types.
Detects:
as { ... }const x: { ... }function foo(param: { ... })): { ... }Allows:
<T extends { ... }>{ [P in keyof T]: ... }Example violations:
// ❌ BAD: Inline object types
function createUser(data: { name: string; email: string }) {
return data
}
const config: { apiKey: string; timeout: number } = {
apiKey: 'xxx',
timeout: 5000
}
// ✅ GOOD: Named types
interface UserData {
name: string
email: string
}
interface Config {
apiKey: string
timeout: number
}
function createUser(data: UserData) {
return data
}
const config: Config = {
apiKey: 'xxx',
timeout: 5000
}
Rationale:
Allow inline types when appropriate:
// @inline-type-allowed
function legacyApi(opts: { debug: boolean }) {
// One-off parameter that won't be reused
}
custom-type-enforcement [options]
| Option | Description | Default |
|---|---|---|
--checks=<checks> | Comma-separated list of checks to run | All checks |
--exclude=<patterns> | Comma-separated glob patterns to exclude files | None |
--help | Show help message | - |
Use --exclude to skip files matching glob patterns. This is useful for test files, generated code, or other files that don't need type enforcement:
# Exclude test files
npx custom-type-enforcement --exclude="**/*.test.ts,**/*.spec.ts,**/__tests__/**"
# Exclude generated files
npx custom-type-enforcement --exclude="**/generated/**,**/*.generated.ts"
# Combine with specific checks
npx custom-type-enforcement --checks=type-exports --exclude="**/*.test.ts"
barrel-files - Prevent pure re-export filestype-exports - Enforce type export architecturetype-imports - Enforce type import architecturetype-duplicates - Warn about duplicate typesinline-types - Discourage inline object typesRun all checks (default):
npx custom-type-enforcement
Run specific checks:
npx custom-type-enforcement --checks=barrel-files,type-exports
npx custom-type-enforcement --checks=type-exports,type-imports
Get help:
npx custom-type-enforcement --help
Add to your package.json scripts:
{
"scripts": {
"check": "custom-type-enforcement",
"check:types": "custom-type-enforcement --checks=type-exports,type-imports,type-duplicates",
"check:quality": "custom-type-enforcement --checks=barrel-files,inline-types",
"precommit": "custom-type-enforcement"
}
}
Add to your GitHub Actions workflow:
name: Code Quality
on: [push, pull_request]
jobs:
quality-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npx custom-type-enforcement
Using husky:
{
"husky": {
"hooks": {
"pre-commit": "custom-type-enforcement"
}
}
}
The tool runs from wherever it's called (uses process.cwd()). For monorepos, delegate checks via npm workspaces.
{
"name": "my-monorepo",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"check": "npm run check --workspaces --if-present",
"check:types": "npm run check:types --workspaces --if-present"
},
"devDependencies": {
"@shaenchen/custom-type-enforcement": "^1.0.0"
}
}
{
"name": "@myorg/api",
"scripts": {
"check": "custom-type-enforcement",
"check:types": "custom-type-enforcement --checks=type-exports,type-imports"
}
}
# Check all workspaces
npm run check
# Check specific workspace
npm run check -w @myorg/api
Suppress false positives using comment directives:
// @barrel-file-allowed
export * from './components'
export * from './utils'
// @type-export-allowed
export interface LegacyType {
// Special case requiring export from this file
}
// Can also be placed on the same line
export type SpecialCase = {}; // @type-export-allowed
// @type-import-allowed
import type { ExternalType } from './wrapper'
// Can also be placed on the same line
import type { LibraryType } from './lib'; // @type-import-allowed
// @type-duplicate-allowed
export interface SpecialCase {
// Intentionally similar to another type
id: string
name: string
}
// @inline-type-allowed
function legacyFunction(opts: { debug: boolean }) {
// One-off parameter
}
The tool uses a minimal, LLM-optimized output format designed for efficient token usage.
✓ All checks passed
✗ 2 checks failed (3 violations)
type-exports (2):
src/services/user.service.ts:5: Type exported from non-types file
src/utils/helpers.ts:12: Interface exported from non-types file
Fix: Move type/interface/enum exports to types.ts or types/{domain}.ts files
Keep functional exports (functions, classes) in implementation files
Use type composition (Pick, Omit, &) to create variations of types
To suppress: Add // @type-export-allowed comment on same line or line above
inline-types (1):
src/app.ts:15: Inline type in function parameter
Fix: Extract inline types to named type aliases or interfaces
Define types in appropriate types.ts files
Use descriptive type names that explain the purpose
To suppress: Add // @inline-type-allowed comment on same line or line above
This format:
Contributions are welcome! Here's how to add a new check:
Create src/checks/my-check.ts:
import { getTypeScriptFiles } from '../lib/get-typescript-files.js'
import { Formatter } from '../lib/formatter.js'
import type { CheckOptions, CheckResult } from '../types.js'
import * as fs from 'fs'
/**
* Run my custom check
*/
export function runMyCheck(options: CheckOptions = {}): CheckResult | void {
const formatter = new Formatter('My Check')
formatter.start()
const files = getTypeScriptFiles({ projectRoot: options.projectRoot })
if (!files) {
console.error('ERROR: No TypeScript files found')
process.exit(1)
}
for (const file of files) {
const content = fs.readFileSync(file, 'utf-8')
const lines = content.split('\n')
lines.forEach((line, index) => {
// Your check logic here
if (/* violation detected */) {
formatter.addViolation({
file,
line: index + 1,
content: line.trim(),
message: 'Violation description',
severity: 'HIGH',
})
}
})
}
const exitCode = formatter.finish({
blocking: true,
noExit: options.noExit,
howToFix: [
'How to fix this issue',
'Alternative solution'
],
whyItMatters: [
'Why this check is important',
'What problems it prevents'
],
})
if (options.noExit) {
return {
checkName: 'my-check',
passed: exitCode === 0,
violationCount: formatter.getViolationCount(),
exitCode,
violations: formatter.getViolations().map(v => ({
file: v.file,
line: v.line,
message: v.message ?? 'Violation detected',
})),
howToFix: [
'How to fix this issue',
'Alternative solution'
],
suppressInstruction: 'To suppress: Add // @my-check-allowed comment on same line or line above',
}
}
}
Update src/cli/index.ts:
import { runMyCheck } from '../checks/my-check.js'
const CHECK_RUNNERS: Record<CheckName, CheckRunner> = {
// ... existing checks
'my-check': runMyCheck,
}
Add to src/types.ts:
export type CheckName =
| 'barrel-files'
// ... existing checks
| 'my-check'
Create tests/my-check.test.ts:
import { describe, it, expect } from 'vitest'
import { runMyCheck } from '../src/checks/my-check'
describe('My Check', () => {
it('should detect violations', () => {
// Test implementation
})
})
Add check description to README.md.
tsconfig.json in root directoryThe tool must be run from a TypeScript project root containing tsconfig.json.
# Make sure you're in the right directory
cd /path/to/your/typescript/project
# Verify tsconfig.json exists
ls tsconfig.json
# Run the tool
npx custom-type-enforcement
Make sure:
package.json--workspaces --if-present in the root scriptdevDependencies)Use ignore comments to suppress false positives:
// @barrel-file-allowed - Allow barrel files// @type-export-allowed - Allow type exports from non-types files// @type-import-allowed - Allow type imports from non-types files// @type-duplicate-allowed - Ignore type duplicates// @inline-type-allowed - Allow inline object typesAll flags can be placed either on the same line or on the line above the code.
MIT © Steve Haenchen
See LICENSE file for details.
Built with ❤️ to enforce code quality and architectural consistency in TypeScript projects.
FAQs
Enforce TypeScript type architecture and code quality rules
The npm package @shaenchen/custom-type-enforcement receives a total of 18 weekly downloads. As such, @shaenchen/custom-type-enforcement popularity was classified as not popular.
We found that @shaenchen/custom-type-enforcement demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Company News
Replit is integrating Socket Firewall into its AI-powered development experience to help protect builders from malicious open source packages.

Security News
npm confirmed a tooling bug incorrectly marked several one-character packages as security holders and said it was working on a rollback.

Research
/Security News
Newer packages in this compromise use native extensions and .pth loaders to execute JavaScript stealers in developer environments.