Civet: A better TypeScript?

Get early access to ECMAScript proposals and slick added features with this modern superset of TypeScript.

Daboost/Shutterstock
"There's a rumor going around that Civet is the new CoffeeScript but maybe that's a good thing. CoffeeScript brought classes, destructuring, async/await, arrow functions, rest parameters, and more to the official JavaScript spec. Maybe Civet will get the pipe operator, pattern matching, and more into ES2025." —Civet creator Daniel Moore

Civet is described as a kind of modern CoffeeScript for TypeScript, which may not sound promising if you remember CoffeeScript as I do. Before you write it off, though, consider what Civet has to offer. This is a compact, modern language that aims to give you everything you like about TypeScript with more power and simplicity, including early access to proposed ECMAScript language features. You could be surprised by some of the capabilities Civet puts into your hands with very little effort. 

What is Civet?

Since Civet is often compared to CoffeeScript, it's helpful to start by considering what they have in common, as well as where they differ.

Like TypeScript, CoffeeScript is a superset of JavaScript. It was released in an era when JavaScript was best known for its many shortcomings, and CoffeeScript was a stopgap to address some of those concerns. JavaScript soon evolved toward greater expressiveness and capability, and CoffeeScript was seen as an unnecessary added layer.

Civet, on the other hand, is designed to be a value-added layer that continually grows and evolves to decorate TypeScript (and JavaScript) code with state-of-the-art capabilities. If you want to try out its additional syntax, you can simply add Civet to your build pipeline, for example with Vite or esbuild.

Since Civet transpiles directly to TypeScript, it has strong dev-time support in the IDE. As Civet developer Erik Demaine says “A huge advantage is the VS Code LSP [Language Server Protocol extension], so while you're editing your file it automatically transpiles and runs TypeScript, underlines errors, and offers hover help.”

In the next sections, we'll take a look at key features of Civet’s syntax. It’s a fairly small chunk to hold in the brain. Keep in mind that valid TypeScript is also valid Civet.

Use Civet to iterate in JSX

Civet can handle JSX. It lets you skip closing tags in markup and closes them for you if you indent the tags. It also automatically wraps multiple sibling elements or fragments into a parent fragment. One of my personal biggest disappointments in using JSX is dealing with iterating over lists. The typical approach involves interweaving JavaScript directly into the markup, as shown in Listing 1.

Listing 1. Iterating in JSX


<For each={props.items}>
  {(item) => {
    return (
      <li class="item" style={props.style}>
        <Item item={item} />
      </li>
    );
  }}
</For>

That seems unnecessarily clunky to me. It’s bearable once you’re used to it and there are other ways to go about iteration (although writing markup in JavaScript is just as clunky). But when all you need to do is iterate over a collection, it’d be nice if it was simple enough to just type it out on the fly.

Listing 2 shows the loop from Listing 1 written using Civet.

Listing 2. Iterating in Civet


<For each=props.items>
   (item) =>
     <li .item {props.style}><Item {item}>

The code in Listing 2 is easier to remember and type out without looking it up.

The Civet pipe operator

Civet gives you the proposed TypeScript pipe operator before it becomes official. The basic idea of this feature is to allow for combining operations without nesting or fluent method chaining. Like CoffeeScript once did for JavaScript, Civet lets you use the feature now, before it officially hits TypeScript.

Using the pipe operator (|>) makes it possible to write chained operations in a manner that is easy to read. 

Let’s say you have several operations defined (say, foo, bar, and baz) and now you want to use them in conjunction to modify a variable. In straight JavaScript, you might wind up with nested function calls like foo(bar(baz(myVar))) or possibly baz(myNum).bar().foo(). Both are clunky, and as things become more complex, they become ever more difficult to decipher. You can execute the same logic in Civet with the pipe operator, as shown in Listing 3.

Listing 3. Using the Civet pipe operator


let foo = function(){ … }
let bar = function(){ … }
let baz = function(x){ … }

let myVar = “some value”;

myVar |> baz |> bar |> foo;

The pipe operator makes it more legible to perform several operations together.

A more powerful switch

Another proposed ECMAScript feature that you can adopt early with Civet is pattern matching. There is quite a lot going into the TC39 proposal, which aims to address the shortcomings of the ECMAScript switch statement. At present, as the proposal states, switch “contains a plethora of footguns such as accidental case fallthrough and ambiguous scoping.” It also severely lacks matching capabilities. 

Whereas the proposal introduces a new keyword, match, Civet applies many of the proposed matching improvements directly to the switch statement. For example, in Civet, you can do as shown in Listing 4, switching on a string using more advanced matching. 

Listing 4. Switching with patterns in Civet


let s = [{type:"text", content:"foobar"},'test2'];
switch s
  ""
    console.log "nothing"
  /\s+/
    console.log "whitespace"
  "hi"
    console.log "greeting"
  [{type: "text", content}, ...rest]
    console.log("leading text", content)

// outputs “leading text foobar”

Civet’s switch statement is quite powerful, going way beyond just adding regex patterns. It is actually able (in the fourth case above) to type check the argument as an array, check the first element as an object, and use the object’s properties to then perform its work. Pretty sophisticated.

Notice also that the switch statement has done away with the break statements, as is recommended in the TC39 proposal. By default, execution will not fall through to the next case.

Loops

Let’s return to the topic of loops, Civet has the ability to simplify looping syntax in some cases. As a quick look, see Listing 5, which loops over an array of integers to create a new array of their squares. The loop is executed in three styles: Civet, programmatic JavaScript, and functional JavaScript.

Listing 5. Looping on array


const list =[1,2,3,4,5,6,7];
// Civet
squares :=
  for item of list
    item * item

// programmatic JS
const squares = (() => {
  const results = [];
  for (const item of list) {
    results.push(item * item);
  }
  return results;
})();

// functional JS
squares = list.map((x) => { return x*x }));

Shorthand for single-argument functions

Civet includes a “single-argument function shorthand” to replace (x) => { }. Listing 6 shows a couple of examples alongside their normal JavaScript equivalents.

Listing 6. Single-arg function shorthand


let x = [{name:'test123'},{name:'Another name'}];

console.log(x.map .name);
console.log(x.map &.name?.slice(0,4));

console.log(x.map((x) => x.name));
console.log(x.map((x) => { return x.name?.slice(0, 4)}));

// output is the same in both groups:
[  "test123",  "Another name" ]
[  "test",  "Anot" ]

Listing 6 shows you how to declare a single function argument without the parenthesis and arrow. The ampersand character in the second example allows reference to the argument.

Slicing arrays and strings

Civet has a handful of shortcuts for slicing arrays and strings. It allows you to use square brackets like function arguments on an array, and the arguments are passed to slice() as you’d expect. As an example, in Listing 7, we perform several slice operations on a string. Notice the added convenience of the brackets.

Listing 7. Slice() shortcuts


let s = "Words are flowing out like endless rain";

console.log(s[10..16]); // outputs “flowing”
// equivalent to console.log(s.slice(10, 1 + 16 || 1 / 0));
console.log(s[-4..]); // output “rain”
// equivalent to console.log(s.slice(-4));
console.log(s[...5]); // outputs “Words”
// equivalent to console.log(s.slice(0, 5));

Here, you can see how the syntax makes it a bit simpler to use slice(). Note that the two-dot and three-dot syntaxes mean exclusive and inclusive of the final element, respectively.

Conclusion

There is more to Civet, but this is a good sampling. See the Civet Cheatsheet for a good overview of Civet's syntax as compared to TypeScript's. See the Civet project on GitHub for a more in-depth look at Civet.

Civet might make the most sense as a laboratory of ideas. It's a good tool if you want to adopt and experiment with new features proposed for the ECMAScript specification before they are released. It's an entry point to the leading edge of development in this space.