Natural language triples for JavaScript & TypeScript

Plain English in. Structured triples out.

nl3 parses short Subject · Predicate · Object phrases into validated triples. Describe your domain with a grammar and a vocabulary — nl3 handles the synonyms, tenses, reversed phrasings, and missing types.

nl3.parse(…) — click a phrase
"user jack contacts user jill"

Three ideas, that’s it

You teach nl3 your domain once, then throw natural phrasings at it.

1

Grammar

Declare valid relations as 'Subject Predicate Object'. Words are singularized, so a rule covers every number form.

2

Vocabulary

Map word stems to predicates so synonyms and tenses — msg, messaged, contacted — all collapse onto one.

3

Parse

Call parse() and get a validated Triple — with reversed phrasings flipped back into shape and missing types inferred.

Built for modern JavaScript

TypeScript-first, ESM-only, dependency-light.

TypeScript-first

Written in strict TypeScript with bundled declarations — Triple, Nl3Options, and friends are all exported.

Typed errors & tryParse

parse() throws a structured Nl3ParseError with .input and .candidate; tryParse() returns null instead.

Type inference

Omit an entity type and nl3 infers it from the grammar — jack contacts jill resolves both ends. An ambiguity policy controls the multi-match case.

Synonyms & tenses

Classic Porter stemming means sent, sends, and mailed fold to one predicate.

Auto-flipping

Reversed phrasings like message 32 created user bob are detected against the grammar and flipped into the correctly oriented triple.

Pluggable tagger

A full POS tagger ships in-box (loaded lazily); pass your own Tagger implementation to extend or replace it.

Fast & JSON-ready

~60–90k parses/sec with a bounded stem cache and ~10ms import. Triples are plain objects — JSON.stringify just works.

ESM & tree-shakable

Pure ES modules, sideEffects: false, Node ≥ 20. Three runtime dependencies, zero vulnerabilities.

Also in Rust

Need the same grammar in a systems stack? The nl3-rs port mirrors this library feature-for-feature.

Show me the code

A grammar, a vocabulary, and a parse.

Quick start

examples/basic.js
import nl3 from 'nl3';

const client = nl3({
  grammar: ['users message users'],
  vocabulary: {
    msg: 'message',     // user jack msgs user jill
    messag: 'message',  // user jack messaged user jill
    contact: 'message', // user jack contacted user jill
  },
});

client.parse('user jack contacts user jill');
// {
//   subject:   { type: 'user', value: 'jack' },
//   predicate: { value: 'message' },
//   object:    { type: 'user', value: 'jill' }
// }

Type inference & ambiguity

examples/ambiguity.js
// 'message' only ever relates users, so bare
// phrases resolve both ends:
client.parse('jack contacts jill');
// subject: { type: 'user', value: 'jack' }  ← inferred
// object:  { type: 'user', value: 'jill' }  ← inferred

// When two rules share a predicate, pick a policy:
const strict = nl3({
  grammar: ['users message users', 'admins message users'],
  vocabulary: { contact: 'message' },
  ambiguity: 'error', // default: 'first-match'
});

try {
  strict.parse('alice contacts bob');
} catch (error) {
  error.predicate;  // 'message'
  error.candidates; // ['user', 'admin']
}

Errors, structured

examples/messenger.js
import nl3, { Nl3ParseError } from 'nl3';

try {
  client.parse('dog jim hates cat sue');
} catch (error) {
  if (error instanceof Nl3ParseError) {
    error.input;     // 'dog jim hates cat sue'
    error.candidate; // the rejected triple
  }
}

// …or skip the try/catch entirely:
const triple = client.tryParse('dog jim hates cat sue');
// => null

Runnable examples

examples/
  • basic One grammar rule; many phrasings → one triple
  • messenger A fuller domain plus both error modes
  • ambiguity first-match vs. error inference policies
  • custom_tagger Supplying your own POS tagger
  • adventure A playable text adventure — grammar as game affordances
node examples/basic.js
node examples/adventure.js

Install

Node.js ≥ 20 · ES modules · TypeScript types included

$ npm install nl3