This file guides AI and human contributors in writing clean, modern TypeScript code. It reflects best practices as of 2025, aligned with our team’s priorities.
- Readability above all else: Code must be easy to follow, even by new contributors or AI tools.
- Concise and clean: Prefer clarity over cleverness. Avoid unnecessary abstraction.
- Modular and testable: Keep functions focused, side-effect-free, and easy to test.
- Functional where helpful: Use
map
,filter
,reduce
, and similar constructs when they simplify logic. - Inline documentation: Use comments and JSDoc to explain purpose, especially for non-obvious logic.
- Use
strict: true
intsconfig.json
. - Prefer
type
for unions and compositions; useinterface
for objects with inheritance or extension. - Use
satisfies
to validate structure while preserving literal types. - Use
as const
for immutable literals. - Avoid default exports; prefer named exports for clarity.
- Group imports: Node/standard → external packages → internal modules.
- Structure files by feature or domain, not by technical type (e.g., "components", "utils").
- Use
readonly
modifiers on objects and arrays to protect data integrity. - Always handle
switch
cases exhaustively with anever
fallback. - Avoid global state or singleton patterns—inject dependencies explicitly.
- Keep function bodies flat and under 30 lines when possible.
- Avoid using
any
. Useunknown
with explicit type narrowing if needed.
- Use pnpm as the package manager for speed and efficiency.
- Lint code using ESLint with:
@typescript-eslint/eslint-plugin
eslint:recommended
plugin:@typescript-eslint/recommended
- Format code with Prettier, but separate from linting responsibilities.
- Add type packages (
@types/*
) asdevDependencies
when working with untyped libraries. - Use a
types/
directory for global types or overrides. - Enable modern ESM support via
"module": "NodeNext"
or"module": "ESNext"
intsconfig
.
- Template literal types for expressive string contracts and mapped keys.
- Variadic tuple types to support flexible argument patterns.
- Conditional types with
infer
to derive or transform types safely. - Mapped types to transform shapes dynamically.
- Utility types: Use built-ins (
Pick
,Omit
,Partial
,Readonly
,ReturnType
,Parameters
) and custom ones as needed. - Use runtime schema validation with
zod
,valibot
, orio-ts
when accepting dynamic input (API responses, config files). - Prefer composition over inheritance in all but the most necessary cases.
- When adopting functional patterns (e.g., with
remeda
,effect
, orfp-ts
), always pair them with clear names and type annotations.
- Code is readable and logically structured.
- Variable and function names are descriptive and intention-revealing.
- Modules are well-scoped (ideally under 200 lines).
- Every exported function includes a JSDoc block.
- Type safety is preserved (no
any
;unknown
is narrowed). - All runtime inputs are validated or sanitized.
- Lint and type checks pass cleanly.
- Unit tests cover new logic and edge cases.
If you're using an AI to assist in writing code, ensure it follows these guidelines:
- Explain complex logic with comments.
- Default to small, composable functions.
- Prefer explicit return types on exported functions.
- Avoid overgeneralizing or generating “magic” code.
- Document assumptions about data shape or behavior.
Got it. Here's the ESLint and TypeScript config added as markdown-embedded code blocks, ready to append to the bottom of your copilot.md
:
// .eslintrc.cjs
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
env: {
node: true,
es2022: true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
'@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }],
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/consistent-type-imports': 'warn',
'no-console': 'warn',
},
};
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": ["ES2022", "DOM"],
"strict": true,
"noImplicitOverride": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"declaration": true,
"outDir": "dist",
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
// .eslintrc.cjs
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
env: {
node: true,
es2022: true,
},
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
'@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }],
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/consistent-type-imports': 'warn',
'no-console': 'warn',
},
};
{
"scripts": {
"dev": "tsx src/index.ts",
"build": "tsc --build",
"typecheck": "tsc --noEmit",
"lint": "eslint . --ext .ts,.tsx",
"fix": "eslint . --ext .ts,.tsx --fix",
"clean": "rm -rf dist",
"start": "node dist/index.js",
"test": "vitest run",
"test:watch": "vitest"
}
}