Anti slop skill for typescript

name anti-slop-typescript
description Simplify TypeScript code by removing defensive over-engineering, fake type safety, unnecessary helpers, redundant runtime checks, and abstraction noise.

TypeScript Anti-Slop

Apply these rules whenever modifying TypeScript or JavaScript code.

The goal is simple, readable, strongly typed code that trusts the type system and validates only at real boundaries.

Core rule

Validate untrusted data once at the boundary. Trust typed application code everywhere else.

Do not repeatedly rediscover types that the application already knows.


1. Do not propagate unknown unnecessarily

Be suspicious of:

value: unknown
data: unknown
payload: unknown
Record

inside normal application code.

If the shape is known, use the actual type.

Bad:

const asDomainMapEventData = (
  value: unknown,
): DomainMapEventData => {
  if (typeof value !== "object" || value === null) {
    return {};
  }

  return value as DomainMapEventData;
};

Good:

const handleDomainMap = (
  data: DomainMapEventData,
) => {
  // ...
};

Fix broad typing at the source instead of adding asX() helpers downstream.


2. Do not replace simple casts with runtime ceremony

Do not assume as is always bad.

Bad cleanup:

return (
  typeof error === "object" &&
  error !== null &&
  "code" in error &&
  error.code === DUPLICATE_KEY_ERROR_CODE
);

when this is sufficient and clearer:

return (
  error as { code?: number }
)?.code === DUPLICATE_KEY_ERROR_CODE;

Use runtime validation when the value is genuinely untrusted.

Do not add five checks merely to avoid one intentional cast.


3. Remove fake validation

This is not real validation:

if (
  typeof value === "object" &&
  value !== null
) {
  return value as Project;
}

Either trust the value:

return value as Project;

at a known integration point,

or validate it properly at the boundary with the project's existing schema library.

Do not write verbose checks that still end with an unsafe cast.


4. Fix the type instead of adding extraction helpers

Bad:

private domainIdFrom(
  document: Record | undefined,
): string | undefined {
  const domain = document?.domain;

  return domain instanceof Types.ObjectId
    ? domain.toHexString()
    : undefined;
}

If the document shape is known, type it correctly:

interface DnsDocument {
  domain: Types.ObjectId;
}

Then:

return change.fullDocument?.domain.toHexString();

Do not add:

domainIdFrom
projectIdFrom
userIdFrom
stringFrom
objectIdFrom
fieldFrom

to compensate for bad upstream typing.


5. Avoid generic converters

Be suspicious of:

toString()
asString()
safeString()
stringifyValue()
normalizeValue()
toNumber()
asBoolean()
toRecord()
asRecord()

Bad:

const stringify = (value: unknown): string => {
  if (
    typeof value === "string" ||
    typeof value === "number"
  ) {
    return `${value}`;
  }

  if (value == null) {
    return "";
  }

  if (value instanceof Types.ObjectId) {
    return value.toHexString();
  }

  return inspect(value);
};

Ask:

What is this value actually supposed to be?

If it is a string, type it as a string.

If it is an ObjectId, type it as an ObjectId.

Do not write universal conversion utilities unless the application genuinely needs arbitrary-value serialization.


6. Do not broaden types for "flexibility"

Bad:

const normalizeDomainName = (
  value: unknown,
): string => {
  if (typeof value === "string") {
    return value.trim().toLowerCase();
  }

  if (typeof value === "number") {
    return `${value}`.trim().toLowerCase();
  }

  return "";
};

A domain name should not randomly be a number.

Prefer:

const normalizeDomainName = (
  domain: string,
): string => {
  return domain.trim().toLowerCase();
};

Do not make function inputs broader than the actual domain model.


7. Avoid silent empty fallbacks

Search for:

return "";
return {};
return [];
return undefined;
return null;

value ?? "";
value || "";
value ?? {};
value || {};
value ?? [];
value || [];

when they hide invalid states.

Bad:

if (!project) {
  return {};
}

If the project is required:

if (!project) {
  throw new ProjectNotFoundError();
}

If it is truly optional, model that in the type.

Do not silently degrade invalid application state into empty values.


8. Do not use optional chaining defensively everywhere

Bad:

data?.project?.config?.domain?.name

when the type guarantees those fields.

Prefer:

data.project.config.domain.name

Use optional chaining only when the value is genuinely optional.

Optional chaining should reflect the domain model, not uncertainty about the codebase.


9. Remove unnecessary runtime checks for typed values

Bad:

if (
  typeof project.id !== "string"
) {
  return "";
}

when:

interface Project {
  id: string;
}

already guarantees it.

Trust the type.

If the type is wrong, fix the type or validate at the boundary.


10. Remove generic record accessors

Be suspicious of:

isRecord()
asRecord()
toRecord()
getString()
getNumber()
getBoolean()
getOptionalString()

Bad:

const project = asRecord(data.project);
const id = getString(project, "id");

Good:

const id = data.project.id;

If the code relies heavily on:

Record

fix the architecture.


11. Type queue events properly

Bad:

type QueuePayload = {
  event: string;
  data: unknown;
};

followed by:

asDomainMapEventData(data)
asDomainUnmapEventData(data)
asProjectSyncEventData(data)

Prefer a discriminated union:

type QueueEvent =
  | {
      event: "domain.map";
      data: DomainMapEventData;
    }
  | {
      event: "domain.unmap";
      data: DomainUnmapEventData;
    }
  | {
      event: "project.ratelimit.sync";
      data: ProjectRatelimitSyncEventData;
    };

Then:

switch (event.event) {
  case "domain.map":
    return handleDomainMap(event.data);

  case "domain.unmap":
    return handleDomainUnmap(event.data);
}

Do not carry unknown into business logic.


12. Validate queue/HTTP/webhook data once

Real boundaries include:

  • HTTP requests
  • webhooks
  • queue messages
  • external APIs
  • environment variables
  • raw database JSON
  • user input

Validate there.

Then convert to a precise internal type.

Use the validation library already present in the codebase.

Do not introduce another schema library without a strong reason.


13. Remove helper explosion

Be suspicious of helpers that:

  • have one caller
  • are 1–3 lines
  • only access a field
  • only call .trim()
  • only call .toLowerCase()
  • only cast a type
  • only check null
  • only return a default
  • only forward arguments

Bad:

const getDomain = (
  data: DomainData,
): string => data.domain;

Prefer:

data.domain

A helper should represent a real concept.


14. Remove pass-through wrappers

Bad:

async getProject(id: string) {
  return this.projectService.getProject(id);
}

when the wrapper adds no business logic, policy, mapping, or meaningful boundary.

Do not preserve layers solely because they exist.


15. Avoid unnecessary DTO duplication

Be suspicious of:

Project
ProjectData
ProjectDTO
ProjectInput
ProjectPayload
ProjectParams
ProjectResponse
ProjectModel

with nearly identical fields.

Separate types when the contracts are materially different.

Do not duplicate shapes just because each layer "should have its own type."


16. Remove mapper/converter slop

Audit functions like:

toDTO
fromDTO
toModel
fromModel
toEntity
fromEntity
toPayload
fromPayload

If the source and target types are effectively identical, remove the unnecessary representation.

Do not maintain mapping code for no semantic reason.


17. Avoid pointless interfaces

Do not create interfaces merely because a class exists.

Bad:

interface ProjectService {
  getProject(id: string): Promise;
}

class ProjectServiceImpl
  implements ProjectService {
}

when there is only one implementation and no meaningful consumer abstraction.

Prefer the concrete class.

Interfaces should solve a real problem.


18. Avoid Java-style architecture

Be suspicious of chains like:

Controller
→ Service
→ Manager
→ Processor
→ Handler
→ Repository
→ Store

when most layers just forward data.

Collapse pass-through layers.

Prefer fewer meaningful layers.


19. Do not over-abstract duplicated code

Two similar lines do not automatically need a generic helper.

Bad:

const normalizeValue = (
  value: T,
  normalizer: (value: T) => T,
): T => normalizer(value);

Prefer obvious duplication over a generic abstraction that makes code harder to trace.


20. Avoid unnecessary generics

Be suspicious of:

function safeCast()
function getValue()
function normalize()
function parseValue()
function ensure()

when concrete types would be clearer.

Generics should solve a real reusable problem.

Do not use them merely to make helpers appear reusable.


21. Avoid unnecessary type aliases

Question aliases like:

type DomainString = string;
type EventValue = unknown;
type GenericData = Record;

Keep aliases when they add domain meaning or improve safety.

Do not create aliases that only rename primitives without value.


22. Prefer discriminated unions

If states are mutually exclusive, model them properly.

Bad:

interface Deployment {
  type: string;
  web?: WebDeployment;
  worker?: WorkerDeployment;
}

Prefer:

type Deployment =
  | {
      type: "web";
      web: WebDeployment;
    }
  | {
      type: "worker";
      worker: WorkerDeployment;
    };

Make impossible states impossible.


23. Avoid defensive String, Boolean, Number

Search for:

String(value)
Boolean(value)
Number(value)
`${value}`
!!value

when the type already guarantees the primitive.

Bad:

const projectId = String(project.id);

when project.id is already a string.

Prefer:

const projectId = project.id;

Do not coerce values unnecessarily.


24. Avoid arbitrary coercion

Bad:

String(undefined)
Number("")
Boolean("false")

can produce misleading values.

Do not use coercion as validation.

Model valid values correctly.


25. Do not normalize values that should never be valid

Bad:

if (typeof value === "number") {
  return String(value);
}

for a domain name, email, UUID, hostname, event name, etc.

Do not make nonsensical values "work."

Reject them at the boundary or prevent them via types.


26. Keep meaningful normalization

This is fine:

const domain =
  input.domain.trim().toLowerCase();

if the input comes from a user-controlled boundary.

The issue is not normalization.

The issue is accepting arbitrary types and silently coercing them.


27. Simplify boolean logic

Bad:

let shouldProcess = false;

if (event) {
  if (event.enabled === true) {
    shouldProcess = true;
  }
}

Prefer:

const shouldProcess =
  event?.enabled === true;

But do not create unreadable boolean one-liners.

Prefer readability.


28. Avoid nested ternaries

Do not use nested ternaries.

Bad:

const value = active
  ? enabled
    ? "a"
    : "b"
  : "c";

Use if, switch, or a simple variable assignment.

Ternaries should be short and obvious.


29. Prefer early returns

Bad:

if (project) {
  if (project.enabled) {
    if (project.status === "active") {
      // 50 lines
    }
  }
}

Prefer:

if (!project) {
  return;
}

if (!project.enabled) {
  return;
}

if (project.status !== "active") {
  return;
}

// main logic

Keep the happy path clear.


30. Remove useless intermediate variables

Bad:

const rawDomain = data.domain;
const normalizedDomain =
  normalizeDomainName(rawDomain);
const domain = normalizedDomain;

Prefer:

const domain =
  data.domain.trim().toLowerCase();

Intermediate variables should clarify meaning, not inflate code.


31. Remove obvious comments

Delete comments that narrate syntax.

Bad:

// Check if project exists
if (!project) {

Bad:

// Convert ObjectId to string
const id = objectId.toHexString();

Keep comments for:

  • business rules
  • unusual constraints
  • workarounds
  • external quirks
  • important invariants

Explain why, not what.


32. Do not catch errors just to hide them

Bad:

try {
  await operation();
} catch {
  return undefined;
}

Bad:

try {
  await operation();
} catch (error) {
  console.log(error);
}

Determine whether the failure is genuinely recoverable.

Do not silently swallow errors.


33. Avoid useless catch-and-rethrow

Bad:

try {
  return await service.run();
} catch (error) {
  throw error;
}

Remove the try/catch.

Only catch when you are:

  • translating the error
  • adding meaningful context
  • cleaning resources
  • intentionally recovering

34. Do not wrap errors at every layer

Avoid:

Failed to process project:
Failed to load project:
Failed to fetch project:
Database error:
actual error

Add context where it materially improves debugging.

Do not mechanically wrap every call.


35. Avoid generic "safe" helpers

Treat names like these as suspicious:

safeGet
safeString
safeNumber
safeParse
safeObject
safeArray
ensureObject
ensureString
ensureArray

They often hide bad typing.

Fix the source type instead.


36. Avoid unnecessary builders/factories

Bad:

new DeploymentBuilder()
  .withProjectId(projectId)
  .withRegion(region)
  .withPort(port)
  .build();

when:

const deployment: Deployment = {
  projectId,
  region,
  port,
};

is clearer.

Likewise, do not add factories unless runtime selection or construction complexity actually exists.


37. Avoid unnecessary dependency injection abstractions

Normal constructor injection is enough:

new ProjectService(
  projectRepository,
  logger,
);

Do not add:

Container
Registry
Provider
Resolver
ServiceLocator

without a real need.


38. Avoid overly generic utility modules

Audit files/folders named:

utils
helpers
common
shared
base
core
misc

Delete trivial helpers.

Move domain-specific helpers to the domain that owns them.

Do not create another generic utility dumping ground.


39. Avoid premature reuse

If two call sites happen to look similar, do not automatically extract them.

Ask:

Is the shared concept real?

If not, leave the code explicit.

Small duplication can be cheaper than a bad abstraction.


40. Avoid unnecessary callbacks

Bad:

processValue(
  value,
  (value) => normalize(value),
);

when:

normalize(value);

is sufficient.

Do not turn normal calls into callback APIs without a reason.


41. Avoid unnecessary Promise wrappers

Bad:

return new Promise(
  async (resolve, reject) => {
    try {
      resolve(await run());
    } catch (error) {
      reject(error);
    }
  },
);

Prefer:

return run();

Do not wrap promises that already exist.


42. Avoid unnecessary async

Bad:

async function getValue() {
  return Promise.resolve(value);
}

or:

async function getProject() {
  return repository.getProject();
}

when no await or async boundary is required.

Remove unnecessary async where it adds no value.


43. Avoid unnecessary spreading/cloning

Be suspicious of:

return {
  ...value,
};

or:

const copy = [...items];

if there is no ownership/mutation reason.

Do not allocate "for safety" without a real requirement.


44. Avoid defensive defaults in constructors

Bad:

constructor(
  private readonly config:
    Config = {} as Config,
) {}

If the dependency is required, require it.

Do not silently construct invalid objects.


45. Avoid optional fields by default

Do not write:

interface Project {
  id?: string;
  name?: string;
  config?: Config;
}

just because data could theoretically be incomplete.

If the application requires them:

interface Project {
  id: string;
  name: string;
  config: Config;
}

Optionality should reflect real business semantics.


46. Avoid nullable unions without reason

Question:

string | null | undefined

when only one absence state is necessary.

Do not proliferate multiple empty states without a contract requiring them.


47. Avoid defensive instanceof chains

Bad:

if (value instanceof ObjectId) {
  // ...
} else if (typeof value === "string") {
  // ...
} else if (typeof value === "number") {
  // ...
}

when the domain says the value is an ObjectId.

Type it correctly.


48. Do not use inspect() as a fallback serializer

If code reaches:

inspect(value)

because the program does not know the value's type, inspect the root cause.

Do not make arbitrary values printable and call that correctness.


49. Do not add logging for every internal step

Bad:

logger.debug("starting project lookup");
logger.debug("project found");
logger.debug("normalizing domain");
logger.debug("mapping domain");

Log meaningful operational events.

Do not narrate function execution.


50. Avoid logging and rethrowing everywhere

Bad:

catch (error) {
  logger.error(error);
  throw error;
}

when the caller also logs the same failure.

Prefer one responsible logging boundary.


51. Avoid classes where plain functions/types are enough

Do not create classes just to group stateless helpers.

Bad:

class DomainUtils {
  static normalize(domain: string) {
    // ...
  }
}

Prefer:

const normalizeDomain = (
  domain: string,
) => {
  // ...
};

Use classes when there is meaningful state or behavior.


52. Avoid excessive private methods

Be suspicious when one class has dozens of tiny private methods like:

getDomainId()
extractProject()
normalizeName()
resolveState()
buildPayload()
prepareData()
formatValue()

If they only make the reader jump around, inline them.

Private methods should clarify meaningful chunks of behavior.


53. Avoid one-use type guards

Bad:

const isDomainEvent = (
  value: unknown,
): value is DomainEvent => {
  // 12 lines
};

when it is used once inside a boundary that already has schema validation.

Do not duplicate schema validation with custom guards.


54. Prefer existing schema validation

If the project already uses:

Zod
Yup
Joi
Valibot
TypeBox
Ajv

use it at untrusted boundaries.

Do not create custom validation utilities beside it.


55. Do not introduce validation libraries unnecessarily

If a simple trusted internal type is sufficient, do not add Zod just to validate internal objects.

Boundary validation and internal typing are different concerns.


56. Avoid Partial abuse

Be suspicious of:

Partial
Partial
Partial

used deep in application logic.

If only some fields are needed, define the actual input type:

type ProjectUpdate = {
  name?: string;
  region?: string;
};

Do not weaken large domain types just for convenience.


57. Avoid Record when keys are known

Bad:

Record

when valid keys are known.

Prefer:

Record

or a typed object.

Make invalid keys impossible where useful.


58. Avoid unsafe as any

Search for:

as any

Fix the type mismatch instead where practical.

Do not use as any merely to silence the compiler.


59. Do not overreact to every cast

At the same time, do not replace a small intentional cast with huge runtime narrowing.

Prefer an honest local cast over fake defensive machinery when the external contract guarantees the shape.

Use judgment.


60. Keep Mongo/Mongoose types concrete

For Mongoose/Mongo code, avoid:

Record
unknown
any

for known document shapes.

Define the document type.

Bad:

const domain =
  document?.domain;

return domain instanceof Types.ObjectId
  ? domain.toHexString()
  : undefined;

when the schema guarantees:

domain: Types.ObjectId

Prefer:

return document?.domain.toHexString();

61. Do not duplicate Mongoose guarantees with runtime checks

If the schema and TypeScript types guarantee a field's type, do not repeatedly check:

instanceof Types.ObjectId
typeof value === "string"

inside normal application code.

Fix schema/type alignment if necessary.


62. Keep API boundaries strict

HTTP handlers should parse/validate external input.

Services should not receive:

unknown
Record

unless they are themselves the parsing boundary.

Prefer:

service.createProject(input);

where input is already typed and validated.


63. Narrow service signatures

Bad:

syncProject(
  payload: ProjectSyncPayload,
)

when the function only needs:

projectId
rateLimit

Prefer:

syncProject(
  projectId: string,
  rateLimit: number,
)

Pass only what the function actually needs.


64. Remove meaningless wrapper types

Bad:

type ExistsResult = {
  exists: boolean;
};

when:

Promise

is sufficient.

Use result objects when multiple related values need to travel together.


65. Avoid unnecessary enums

Do not turn every string into an enum.

Use enums or literal unions when there is a real closed set of values.

Prefer:

type Status =
  | "pending"
  | "running"
  | "failed";

when that fits the codebase.

Do not create abstractions around arbitrary strings for no reason.


66. Prefer simple switch over dispatch architecture

Do not turn:

switch (event.type) {
  case "insert":
  case "update":
  case "delete":
}

into:

HandlerRegistry
EventProcessor
StrategyFactory
EventHandlerInterface

unless dynamic registration is actually needed.

A switch is fine.


67. Avoid unnecessary strategy patterns

Three branches do not automatically need three classes.

Prefer direct control flow when easier to understand.


68. Do not abstract standard library behavior

Avoid custom wrappers around:

JSON.parse
Array.isArray
Object.keys
Object.entries
String.prototype.trim
String.prototype.toLowerCase
Map
Set

unless the wrapper adds meaningful domain behavior.


69. Remove dead abstractions after refactoring

If fixing a type makes these obsolete:

asX
isX
normalizeX
safeX
convertX
extractX

delete them.

Do not leave compatibility helpers behind unless they still have real callers and purpose.


70. Review code added by AI agents aggressively

Treat these patterns as suspicious:

const asX = (
  value: unknown,
): X => ...
const safeX = ...
const normalizeX = (
  value: unknown,
) => ...
if (
  typeof value === "object" &&
  value !== null &&
  "foo" in value
)
value ?? ""
value ?? {}
value ?? []
return inspect(value)
return String(value)
Record

They are not automatically wrong, but they deserve scrutiny.


71. Do not make cleanup increase complexity

A refactor is suspicious if it turns:

return (
  error as { code?: number }
)?.code === DUPLICATE_KEY_ERROR_CODE;

into:

return (
  typeof error === "object" &&
  error !== null &&
  "code" in error &&
  typeof error.code === "number" &&
  error.code === DUPLICATE_KEY_ERROR_CODE
);

without adding meaningful correctness.

Likewise, replacing:

document.domain.toHexString()

with:

this.extractAndNormalizeDomainId(
  document,
)

is not an improvement.


72. Fix the root cause

Whenever you see:

asSomething
safeSomething
normalizeSomething
extractSomething
resolveSomething
convertSomething

trace the value upstream.

Ask:

  1. Why is the value broad?
  2. Where does it enter the application?
  3. Is that the correct place to validate it?
  4. Can the downstream type become precise?
  5. Can the helper then disappear?

Prefer fixing the earliest sensible point in the data flow.


73. Preserve real defensive checks

Do not remove checks that protect against genuine uncertainty.

Keep handling for:

  • external API failures
  • database not-found cases
  • nullable database fields
  • malformed HTTP input
  • malformed queue messages
  • optional business fields
  • Redis misses
  • JSON parse failures
  • security checks
  • authorization checks
  • external library behavior that is actually broad

The distinction is:

Defend against external uncertainty, not against your own typed application code.

74. Preserve behavior

Do not:

  • change business logic
  • change public API behavior
  • change event names
  • change persistence formats
  • remove security checks
  • remove legitimate nullability
  • introduce new dependencies without strong justification

This is a complexity reduction exercise.


75. Desired style

Prefer:

const handleDomainMap = async (
  data: DomainMapEventData,
) => {
  const domain =
    data.domain.trim().toLowerCase();

  await domainService.map({
    projectId: data.projectId,
    domain,
  });
};

over:

const handleDomainMap = async (
  rawData: unknown,
) => {
  const data =
    asDomainMapEventData(rawData);

  const projectId =
    asOptionalString(data.projectId);

  const domain =
    normalizeDomainName(
      getSafeValue(data.domain),
    );

  if (!projectId || !domain) {
    return;
  }

  await domainService.map({
    projectId,
    domain,
  });
};

76. Mandatory before-change review

Before editing code:

  1. Identify the trusted and untrusted boundaries.
  2. Understand the actual domain types.
  3. Check whether broad types are accidental.
  4. Avoid introducing helpers before understanding upstream typing.
  5. Prefer modifying the root type/data flow over patching call sites.

77. Mandatory after-change review

Before finishing any TypeScript change, inspect the diff.

For every added helper, type, branch, fallback, cast, or runtime check, ask:

  1. Does this handle a state that can genuinely occur?
  2. Is this compensating for bad typing upstream?
  3. Could the type system express this instead?
  4. Did I add more code than the problem requires?
  5. Did I introduce a helper with only one trivial caller?
  6. Did I replace a direct operation with an abstraction?
  7. Did I silently convert invalid input into an empty value?
  8. Did I make the code harder to trace?
  9. Did this change increase total complexity?
  10. Can any newly added code be deleted while preserving correctness?

If yes, simplify before completing the task.


78. Final anti-slop rule

Do not transform:

simple typed value
→ direct operation

into:

unknown
→ runtime narrowing
→ Record
→ extractor
→ normalizer
→ fallback
→ helper
→ actual operation

The desired flow is:

untrusted input
→ validate once
→ precise type
→ direct business logic

The overriding principle is:

Make external input safe at the edge. Keep internal TypeScript simple, direct, and strongly typed.
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论