TypeScript
The TypeScript package is TDC's reference implementation. The CLI is the right tool when you want a file; the library is for pulling data straight into your code — as a string or as live JS objects — without spawning a process or reading a file.
import { TDC } from "tdcv2";
Creating a TDC
The constructor takes either a path to a DSL file (configFile) or a DSL string
(configString). You can override the runtime parameters seed, count, locale,
and now from code, and those values win over the ones in <env>.
const tdc = new TDC({
configString: `<tdc>
<env count="4" seed="demo" local="en">
<sequence name="Gender"><gen type="text" value="Male,Female"/></sequence>
<sequence name="MaleName" parent="Gender.Male"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="FemaleName" parent="Gender.Female"><gen type="template" value="person.female.firstName"/></sequence>
<before><line><data>Gender,Name</data></line></before>
</env>
<block><line><data>\${{Gender}},\${{MaleName}}\${{FemaleName}}</data></line></block>
</tdc>`,
});
console.log(tdc.toString());
The name is tied to the gender through parent — two sequences, one per branch.
Without that, the name would be drawn independently and a man would end up with a
woman's name. Exactly one of the two is filled on any given row, so the template
can put them back to back and still print a single name.
Gender,Name Female,Mary Male,James Male,John Female,Elizabeth
Overriding from code — these take precedence over <env>:
const tdc = new TDC({
configFile: "./patients.tdc",
seed: "test-seed",
count: 100,
locale: "ru",
});
If the config reads from external files, point the library at the data folders (and
give it a base directory when you're using configString):
const tdc = new TDC({
configFile: "./configs/users.tdc",
dataPaths: ["./data", "./private-data"],
});
With configFile, relative src paths inside the .tdc are resolved against that
file's folder. With configString there's no file to resolve against, so set
baseDir yourself.
The rest of the constructor's options are less common but no less real:
| Option | What it does |
|---|---|
locale | Overrides <env local=…> — the config loses |
defaultLocale | Only fills in when <env> declares no locale at all. The locale key in tdcv2.config.json becomes this, not the override |
mode | "memory" or "disk" — the same choice as --mode |
engine | 1, 2 or 3 — forces one engine, and fails rather than falling back |
stream | Legacy alias for engine: 2 |
That locale / defaultLocale split is worth a second look: a "locale": "ru" in the
project config does not beat a local="en" in the config file, and nothing reports
it. Only the constructor's own locale and the CLI's --locale override.
preflight(opts?) takes one option of its own: output, either "materialized" (the
default — the whole run held at once, what toArray() does) or "streaming".
Terminal methods
| Method | Returns | For |
|---|---|---|
toString() | the whole output as one string | small / medium results |
writeFile(path) | writes the output to a file (chunks) | a file of any size |
toIterator() | a generator of lines (one per record) | large text, no full string |
toStream() | a Node.js Readable | pipe to a file / HTTP / gzip |
toColumns() | columns; numbers as Float64Array (double[] in Java and C#, Vec<f64> in Rust, array('d') in Python) | numeric pipelines, many runs |
toArray() | an array of row objects | small object fixtures |
iterate() | a generator of row objects | object output, no array |
getAt(index) | one row object by index | point access |
preflight(opts?) | a memory diagnostic, or undefined | a check before a big run |
seedInfo() | { seed, generated } | read / log the seed |
toStringAsync() | the whole output, as a promise | a config with type="http" |
writeFileAsync(path) | writes the output, as a promise | a config with type="http" |
usesHttp() | true when the config makes a call | choosing between the two pairs |
toString, writeFile, toIterator, and toStream all produce text through the
disk-backed engine, and their memory use is O(number of fields). See Large
outputs for measurements.
One rule about the async pair: a config holding an
<gen type="http"> must use it. A network call cannot be
made from a synchronous function, so toString() on such a config throws rather than
returning half a dataset:
const tdc = new TDC({ configFile: "./enriched.tdc" });
const text = tdc.usesHttp() ? await tdc.toStringAsync() : tdc.toString();
usesHttp() answers that question without running anything, which is exactly what the
CLI does. Everything else behaves identically on both paths — for a config with no http
generator, toStringAsync() is toString() wrapped in a promise. What does change is
reproducibility: a run that calls a service is only as repeatable as the service is.
Object output
In tests it's usually easier to work with live objects than to parse CSV or JSON —
you can assert on row.Gender directly. toArray(), iterate(), and getAt(index)
give you that. Object output ignores <block> and the text wrappers; it reads
only the materialized <sequence>s:
- a simple sequence becomes a scalar property;
- a compound sequence becomes a nested object;
- a parent-filtered sequence is
undefinedon rows where it doesn't apply.
getAt(index) is one row's work and the index does not matter: on a 200 000-row
config, getAt(0) and getAt(199999) both return in about 2 ms, where toArray() takes
around 210 ms for the same run. iterate() costs the same as toArray() in total but
never holds the array.
const tdc = new TDC({
configString: `<tdc>
<env count="4" seed="demo" local="en">
<sequence name="Gender"><gen type="text" value="Male,Female"/></sequence>
<sequence name="Person">
<gen name="Code" type="regex" value="[0-9]{4}"/>
</sequence>
<sequence name="MaleName" parent="Gender.Male"><gen type="template" value="person.male.firstName"/></sequence>
<sequence name="FemaleName" parent="Gender.Female"><gen type="template" value="person.female.firstName"/></sequence>
</env>
<block><line><data>ignored</data></line></block>
</tdc>`,
});
console.log(tdc.getAt(0)); // a Female row
console.log(tdc.getAt(1)); // a Male row
{
Gender: 'Female',
Person: { Code: '7541' },
MaleName: undefined,
FemaleName: 'Mary'
}
{
Gender: 'Male',
Person: { Code: '1506' },
MaleName: 'James',
FemaleName: undefined
}Person is a nested object. MaleName and FemaleName are both present, but only
one is filled on each row; the other is undefined, because its parent didn't match
on that row. That's what a parent filter looks like in object output.
The object methods read from whichever engine the config routes to — the same one
toString() uses — so the values agree, and getAt(index) costs one row rather
than the whole run before it. Asking for row nine million of a ten-million-row
config is a single row's work.
One value, without a config
The package also exports tdc, which draws a single value from the same data packs
a config reads — no file, no <env>, one call:
import { tdc } from "tdcv2";
tdc.person.lastName(); // Jones
tdc.country.usa.docs.ssn(); // 699209702, with its real check digits
tdc.person.lastName.many(5); // five of them
tdc.seed("demo").locale("ru").person.lastName(); // pinned and in Russian
Every bundled address is a real property on the type, so a misspelling is a compile error and completion works with no plugin. One value at a time is the whole surface.
The same names in every language
The object a run hands back answers to the same names in all five packages, spelled each language's own way. The table is here, and the test suites check it rather than take it on trust.
See also
- CLI — the same engine from the command line.
- Large outputs — streaming methods and memory.