npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@shaenchen/custom-type-enforcement

v1.0.1

Published

Enforce TypeScript type architecture and code quality rules

Readme

@shaenchen/custom-type-enforcement

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.

npm version License: MIT

Philosophy: Opinionated by Design

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.

Why These Rules?

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.

Not a One-Size-Fits-All

You don't need to adopt all checks. Use --checks to run only what makes sense for your project:

  • Just want type organization? Run --checks=type-exports,type-imports
  • Just want to reduce duplication? Run --checks=type-duplicates
  • Have legitimate use cases for barrel files? Skip the barrel-files check

These 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.

Features

  • 📦 Architecture: Enforce clean type organization and imports
  • 🔍 Quality: Prevent code smells like barrel files and inline types
  • ♻️ Maintainability: Reduce type duplication across your codebase
  • 🚀 Fast: Minimal dependencies, runs in seconds
  • 🎯 Configurable: Run all checks or selectively via CLI flags
  • 🌳 Monorepo-ready: Works with npm workspaces and multi-package repos

Table of Contents

Installation

npm install -D @shaenchen/custom-type-enforcement

Or use directly with npx:

npx @shaenchen/custom-type-enforcement

Quick Start

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"
  }
}

Available Checks

1. Barrel Files Check

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:

  • Makes dependencies explicit
  • Prevents circular dependencies
  • Improves tree-shaking
  • Clearer code navigation

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'

2. Type Exports Check

Purpose: Enforce that types/interfaces/enums are only exported from types.ts or types/{domain}.ts files.

Rules:

Allowed:

  • Export types from types.ts files
  • Export types from types/{domain}.ts files
  • Export functions/classes from any file
  • Define types locally (non-exported) in any file

Forbidden:

  • Export types from non-types files
  • Export non-functional constants from non-types files
  • export type * pattern (discouraged everywhere)

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.

Allow type exports when needed:

// @type-export-allowed
export interface LegacyType {
  // Special case that needs to be exported from this file
}

3. Type Imports Check

Purpose: Enforce that type imports only come from types.ts or types/{domain}.ts files.

Rules:

Allowed:

  • Import types from types.ts files
  • Import types from types/{domain}.ts files
  • Import types from external packages (npm)

Forbidden:

  • Import types from implementation files
  • Import types from non-types files

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

Allow type imports when needed:

// @type-import-allowed
import type { SpecialType } from './external-library-wrapper'

4. Type Duplicates Check

Purpose: Warn about structurally similar types that could be consolidated.

Detects:

  1. Exact structural matches (field order independent)
  2. Optional field variance (same required fields, different optionals)
  3. Composition opportunities (subset relationships → use Pick/Omit)
  4. Required<T> opportunities (same fields, different optionality)
  5. Intersection opportunities (could use 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:

  • Ignores types with < 2 fields (too generic)
  • Ignores types in the same file (already co-located)
  • Only checks types in types.ts files

5. Inline Types Check

Purpose: Discourage inline object types in favor of named interfaces/types.

Detects:

  • Type assertions: as { ... }
  • Variable declarations: const x: { ... }
  • Function parameters: function foo(param: { ... })
  • Return types: ): { ... }

Allows:

  • Generic constraints: <T extends { ... }>
  • Mapped/conditional types: { [P in keyof T]: ... }
  • Named type/interface declarations

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:

  • Named types have better error messages
  • More searchable and refactorable
  • Self-documenting (type name explains intent)
  • Can be reused across the codebase

Allow inline types when appropriate:

// @inline-type-allowed
function legacyApi(opts: { debug: boolean }) {
  // One-off parameter that won't be reused
}

CLI Options

custom-type-enforcement [options]

Options

| Option | Description | Default | |--------|-------------|---------| | --checks=<checks> | Comma-separated list of checks to run | All checks | | --help | Show help message | - |

Available Checks

  • barrel-files - Prevent pure re-export files
  • type-exports - Enforce type export architecture
  • type-imports - Enforce type import architecture
  • type-duplicates - Warn about duplicate types
  • inline-types - Discourage inline object types

Examples

Run 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

Configuration

package.json Integration

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"
  }
}

CI/CD Integration

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

Pre-commit Hook

Using husky:

{
  "husky": {
    "hooks": {
      "pre-commit": "custom-type-enforcement"
    }
  }
}

Monorepo Usage

The tool runs from wherever it's called (uses process.cwd()). For monorepos, delegate checks via npm workspaces.

Root package.json

{
  "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"
  }
}

Workspace package.json

{
  "name": "@myorg/api",
  "scripts": {
    "check": "custom-type-enforcement",
    "check:types": "custom-type-enforcement --checks=type-exports,type-imports"
  }
}

Running checks

# Check all workspaces
npm run check

# Check specific workspace
npm run check -w @myorg/api

Ignore Patterns

Suppress false positives using comment directives:

Barrel Files Check

// @barrel-file-allowed
export * from './components'
export * from './utils'

Type Exports Check

// @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 Imports Check

// @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 Duplicates Check

// @type-duplicate-allowed
export interface SpecialCase {
  // Intentionally similar to another type
  id: string
  name: string
}

Inline Types Check

// @inline-type-allowed
function legacyFunction(opts: { debug: boolean }) {
  // One-off parameter
}

Output Format

The tool uses a minimal, LLM-optimized output format designed for efficient token usage.

Success (all checks pass):

✓ All checks passed

Failures (with violations):

✗ 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:

  • Uses 80-90% fewer tokens than the previous format
  • Shows only failed checks
  • Groups violations by check type
  • Provides actionable fix instructions
  • Includes suppress instructions at the end of each Fix section

Contributing

Contributions are welcome! Here's how to add a new check:

1. Create Check Implementation

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',
    }
  }
}

2. Add to CLI

Update src/cli/index.ts:

import { runMyCheck } from '../checks/my-check.js'

const CHECK_RUNNERS: Record<CheckName, CheckRunner> = {
  // ... existing checks
  'my-check': runMyCheck,
}

3. Update Types

Add to src/types.ts:

export type CheckName =
  | 'barrel-files'
  // ... existing checks
  | 'my-check'

4. Write Tests

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
  })
})

5. Update Documentation

Add check description to README.md.


Requirements

  • Node.js: >= 18.0.0
  • TypeScript: Project must have tsconfig.json in root directory

Troubleshooting

Error: No tsconfig.json found

The 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

Monorepo: Checks not running in workspaces

Make sure:

  1. Each workspace has the check script in its package.json
  2. You're using --workspaces --if-present in the root script
  3. The tool is installed (either in root or workspace devDependencies)

False positives

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 types

All flags can be placed either on the same line or on the line above the code.


License

MIT © Steve Haenchen

See LICENSE file for details.


Links


Built with ❤️ to enforce code quality and architectural consistency in TypeScript projects.