Skip to main content

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.

node example.js
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:

OptionWhat it does
localeOverrides <env local=…> — the config loses
defaultLocaleOnly 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
engine1, 2 or 3 — forces one engine, and fails rather than falling back
streamLegacy 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

MethodReturnsFor
toString()the whole output as one stringsmall / 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 Readablepipe 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 objectssmall object fixtures
iterate()a generator of row objectsobject output, no array
getAt(index)one row object by indexpoint access
preflight(opts?)a memory diagnostic, or undefineda check before a big run
seedInfo(){ seed, generated }read / log the seed
toStringAsync()the whole output, as a promisea config with type="http"
writeFileAsync(path)writes the output, as a promisea config with type="http"
usesHttp()true when the config makes a callchoosing 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 undefined on 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
node objects.js
{
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.

Same values, one row at a time

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.