Ts ddd entity
Create, review, or guide Domain Entity implementation in a TypeScript + DDD codebase. Use when: the request touches `*.entity.ts` files under `apps/api/src/<bc>/domain/entities/` or their tests under `apps/api/test/<bc>/domain/entities/`; modeling business rules with `Entity` from `@acme/shared` (dual API `create`/`tryCreate`, `Result` / `Result.combine` validation); validating FK refs with `Id.required` or own id with `Id.tryCreate`; enforcing enum-backed closed-set fields (status / kind / layout / provider / palette) from `@acme/<bc>-contracts` instead of string literals; validating nested entities or arrays element-by-element; adding state transitions via `cloneWith` (leaf entity) or named domain methods like `publish`, `addSection`, `deactivate` that mutate `_field` + `touch()` (aggregate root); exposing typed getters or `toSnapshot()` for persistence boundaries.From its SKILL.md
npx -y skills add llodev/skills --skill ts-ddd-entityAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 2 stars2 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
6.2 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
TypeScript DDD Entity
MANDATORY — READ ENTIRE FILE: Before any implementation step, read
references/entity-pattern.md completely.
Do NOT load other DDD skills (use-case, repository, dto) unless explicitly requested.
Before You Start
Before writing a single line, answer:
- Bounded context: which BC owns this entity? File lands at
apps/api/src/<bc>/domain/entities/<name>.entity.tsand is re-exported by that folder'sindex.tsbarrel. - Identity: is
idoptional on create (useId.tryCreate/ let theEntitybase auto-generate viaId.create(props.id!)) or a required foreign relation (validate withId.required)? - Closed-set fields: every status / kind / layout / provider / palette / discriminator must come from a string-backed TS enum in
libs/contracts/<bc>/src/interfaces/orlibs/shared— never inline string literals oras consttuples. - Invariants: which VO (
Slug,PaletteKey,ImageRefValue, …) validates each field? Are there arrays of IDs or nested entities (e.g.Section[]insideCelebration)? - State transitions: does behavior require a domain method? Use
cloneWithfor immutable swap-and-revalidate, or mutate_field+this.touch()for entities that own a mutable collection (seeCelebration). - Constructor visibility:
privatefor leaf entities;protectedonly when subclasses need access.
Core Rules
- Extend
Entity<Type, Props>from@acme/shared; keep constructorprivateorprotected. - Import enums and tagged unions from
@acme/<bc>-contracts(e.g.@acme/celebrations-contracts). Never redefine wire types in the entity file. - Expose dual API:
tryCreate(props): Result<T>(returns Result, canonical) andcreate(props): T(delegates totryCreate+throwIfFailed). - Validate every field via VOs / type guards + collect errors (either
Result.combine([...])or a manualerrors: string[]accumulator — both patterns exist in this codebase; useResult.combinewhen all checks returnResult<T>). - Always store normalized values. Spread
vo.instance.valuefor scalars; for sibling entities, prefer building fromSection.tryCreate(sp)and keeping the array asSection[]in a private field while keepingSectionProps[]inpropsfor serialization. - Getters expose domain values;
this.propsis never accessed from outside the entity class. cloneWith(overrides)deep-merges and re-runstryCreateautomatically — never calltryCreateby hand from a domain method whencloneWithsuffices.
Enum Rule (HARD)
Every closed set is a string-backed TS enum in the contracts package. Pattern:
export enum CelebrationStatusEnum {
DRAFT = "draft",
PUBLISHED = "published",
}
export const CELEBRATION_STATUSES = Object.values(CelebrationStatusEnum);
export type CelebrationStatus = (typeof CelebrationStatusEnum)[keyof typeof CelebrationStatusEnum];
export function isCelebrationStatus(v: unknown): v is CelebrationStatus {
return typeof v === "string" && CELEBRATION_STATUSES.includes(v as CelebrationStatus);
}
Inside the entity:
- Validate with the type guard (
isCelebrationStatus(props.status)→ push"INVALID_CELEBRATION_STATUS"on failure). - Store the value typed as the union (
CelebrationStatus), emit it from a getter. - Compare with the enum member:
this._status === CelebrationStatusEnum.PUBLISHED. Never=== "published". - Singleton / limit catalogs reference enum members:
const SINGLETON_KINDS = [SectionKindEnum.HERO, SectionKindEnum.GALLERY, SectionKindEnum.SIGNATURE] as const.
Base Class Behaviour You Must Know
Entity's protected constructor calls Id.create(props.id!, { attribute: "id" }) and stores the normalized id, plus initializes createdAt/updatedAt/deletedAt if absent. Therefore:
- Do not set
createdAt,updatedAt,deletedAtinsidetryCreate. Usethis.props.updatedAt = new Date()via a privatetouch()for mutations. cloneWithusesstructuredCloneonpropsbefore deep-merging, so nested objects are safe from caller mutation.
NEVER
- NEVER use a raw string literal where an enum member exists (
"published"→CelebrationStatusEnum.PUBLISHED). - NEVER store raw VO input in
props— always normalize viavo.instance.value. - NEVER add a public setter — mutate state through a named domain method.
- NEVER skip validating array elements — loop and either push into a manual
errors[]orResult.combineper-element results. - NEVER put a domain invariant in the use case if it must hold for the entity from any caller.
- NEVER expose
this.propsto callers outside the entity — use typed getters or a deliberatetoSnapshot()method. - NEVER import from
@ddd/shared(legacy alias). The shared lib is@acme/shared.
References
See references/entity-pattern.md for: real paths, canonical tryCreate snippet, enum-driven validation, array / nested-entity pattern, cloneWith vs mutable-collection mutation, test layout under apps/api/test/, and the pitfalls table.
See also examples/product.entity.ts and examples/product.entity.test.ts for a self-contained reference entity with an enum-driven status field.
What ships with it: 9 files
44.5 KB alongside SKILL.md, 2 of them executable
docs/
- i18n/README.es-ES.md5.2 KB
- i18n/README.pt-BR.md5.2 KB
examples/
- product.entity.test.tsruns4.0 KB
- product.entity.tsruns3.6 KB
references/
- entity-pattern.md18.9 KB
- CHANGELOG.md1008 B
- LICENSE1.1 KB
- package.json792 B
- README.md4.9 KB
Gives 0 of the 12 instructions most test skills give in ~1.3k tokens
Counted across 1,201 of the 2,096 authors here whose files we hold, read 2026-09-06
- Write a failing test before writing codein 43 of 1201, across 36 files
- Run the full test suitein 36 of 1201, across 35 files
- Test only one variable per experimentin 34 of 1201, across 17 files
- Read product marketing context before asking questionsin 34 of 1201, across 14 files
- Mock external dependenciesin 34 of 1201, across 30 files
- Define primary, secondary, and guardrail metricsin 33 of 1201, across 16 files
- Pre-determine sample size before startingin 31 of 1201, across 14 files
- Test behavior rather than implementationin 31 of 1201, across 29 files
- Formulate a hypothesis before designing a testin 30 of 1201, across 13 files
- Document every test hypothesis, variant, and resultin 29 of 1201, across 11 files
- Use descriptive test function namesin 25 of 1201, across 21 files
- Commit to the methodology without stopping earlyin 24 of 1201, across 8 files
Said here and by no other author read
- Read the entity pattern reference before starting
- Extend Entity from the shared library
- Expose tryCreate and create methods
- Validate all fields using value objects or type guards
- Use Result.combine to collect validation errors
- Store normalized values in props
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.