Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Weft The book

Custom types

Give a shape a name in a node’s metadata.json, and every node in the project can use it.

"types": {
  "ChatMessage": "{ role: String, content: String }",
  "ChatHistory": "List[ChatMessage]"
}

Then in the ports of any node in the project, including nodes from other packages:

"inputs": [ { "name": "messages", "type": "ChatHistory", "required": true } ]

A named type is only equal to itself. Declare "CustomerId": "String" and a port that wants a CustomerId refuses a plain String unless the program puts a Cast in between, while a CustomerId goes into a port that wants a String with nothing in between. For how names compare and what fits into what, go and read types.

One table for the whole project

Every metadata.json in the project adds its types to one table before any port is read, and the program’s own .weft files can use these names too. If several nodes in a package share a type, declare it once in the package root’s metadata.json.

Two files may declare the same name with the same shape: the two declarations count as one. If two metadata.json files declare it with different shapes, the nodes of both are left out of the catalog, each with an error saying the name is declared twice with different bodies.

A type written in a .weft file is stricter: it may never reuse a name the program can already see, including the names metadata.json files declare, even to declare the same shape.

Records that take extra keys

A record refuses any key it does not declare. If your node’s type describes data that comes from outside and may carry more than you read (a provider’s answer, a webhook’s body), end it with ..:

"types": { "Charge": "{ id: String, amount: Number, .. }" }

Such a record accepts a value with extra keys. weft drops them when your node emits the value and when a Cast produces it, so what flows on carries only id and amount. A missing field or a field of the wrong type still fails.

What travels

The name carries its shape with it. A port typed ChatMessage is stored as ChatMessage={role: String, content: String}, so anything reading a saved project or a journal row understands it with no table to consult, and a run that started yesterday keeps the shape it started with even if you edit the declaration today.

If you write that stored form into a port’s type yourself and its shape differs from the declaration ("type": "ChatMessage={role: String}"), the build fails with named-type-conflict.

Making your node a cast

If your node converts its input into whatever type the program gives its output, the way Cast does, name the two ports here:

"features": { "castPorts": { "input": "value", "output": "value" } }

Where the node is used, the pair of types its two ports end up with has to be a conversion Cast knows (for the table of those, go and read Cast), and any other pair is the cast-not-allowed error when the program compiles:

no cast from JsonDict into Number; a cast parses text, stringifies data, or
validates an object shape against a declared type

Your node does the conversion itself when it runs, the way Cast does:

let target = ctx.output_type("value")?;
let cast = target
    .cast_value(&value)
    .node_err(format!("casting the value into {target}"))?;