Skip to content


Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?


Failed to load latest commit information.
Latest commit message
Commit time
September 21, 2022 16:14
January 12, 2023 09:40
January 17, 2023 17:03
February 2, 2023 16:04
January 11, 2023 13:51
February 2, 2023 10:35
February 2, 2023 16:30
February 2, 2023 16:30
January 9, 2023 19:30
November 27, 2022 16:45
February 2, 2023 14:48
January 16, 2023 08:57


Build Status NPM Version NPM Downloads Discord Open Collective

The modern way to write TypeScript.

Quickstart Guide

# Install
npm install -g @danielx/civet
# Run Civet code directly in a REPL
# Transpile typed Civet code into TypeScript in a REPL
civet -c
# Compile Civet source file to TypeScript
civet < source.civet > output.ts
# Execute a simple .civet script (no imports)
civet source.civet ...args...
# Execute a .civet source file in node using ts-node
node --loader ts-node/esm --loader @danielx/civet/esm source.civet


Code Sample

ts, {CompilerOptions} from typescript

DefaultCompilerOptions : CompilerOptions :=
  allowNonTsExtensions: true
  allowJs: true
  target: ts.ScriptTarget.Latest
  moduleResolution: ts.ModuleResolutionKind.NodeJs
  module: ts.ModuleKind.CommonJS
  allowSyntheticDefaultImports: true
  experimentalDecorators: true

fileCache : Record<string, any> := {}

createCompilerHost := (options: CompilerOptions, moduleSearchLocations : string[]) ->
  fileExists := (fileName: string) : boolean ->

  readFile := (fileName: string) ->


Civet is essentially a tasteful superset of TypeScript.

Implementations of New and Proposed ES Features

  • Pipe operator (based on F# pipes, Hack pipes and the TC39 proposal)
    • data |> Object.keys |> console.log equivalent to console.log(Object.keys(data))
    • Use single-argument arrow functions or & shorthand to specify how to use left-hand side
    • |> await, |> yield, and |> return (at end) for wrapping left-hand side with that operation
  • Short function block syntax like Ruby symbol to proc, Crystal, Elm record access
    • Access: & =>
    • Nested access + slices: &.profile?.name[0...3] => a.profile?.name.slice(0, 3))
    • Function call: &.callback a,$ => $.callback(a, b))
    • Unary operators: !!&$ => !!$)
    • Binary operators: &$ => $+1)
  • Flagging shorthand based on from LiveScript: {+debug, -live, !verbose}{debug: true, live: false, verbose: false}

Convenience for ES6+ Features

  • Const assignment shorthand: a := bconst a = b, {a, b} := cconst {a, b} = c
  • Let assignment shorthand: a .= blet a = b
  • Typed versions of above: a: number .= 5let a: number = 5 (but note that a: number = 5 is the object literal {a: (number = 5)}).
  • @#idthis.#id shorthand for private identifiers
  • import shorthand: x from ./ximport x from "./x"
  • Dynamic import shorthand: import './x' not at top level (e.g. await import './x' or inside a function) → import('./x')
  • Optional import rename syntax that corresponds to destructuring rename import {x: y} from "./z"import {x as y} from "./z". You can still use as to be compatible with existing ES imports.
  • export shorthand: export x, yexport {x, y}
  • Triple backtick Template Strings remove leading indentation for clarity
  • Class constructor shorthand @( ... )
  • ClassStaticBlock @ { ... }
  • < as extends shorthand
  • /// Block RegExp like Python re.X

JSX Enhancements

Inspired by solid-dsl discussions and jsx spec issues

  • Indentation: instead of explicitly closing <tag>s or <>s, you can indent the children and Civet will close your tags for you
  • Multiple adjacent elements and/or fragments get automatically combined into a fragment.
  • Arrow function children do not need to be wrapped in braces (assuming they are not preceded by text); this is unambiguous because > isn't valid JSX text. For example, <For> (item) => ... (where function body can be indented).
  • #foo shorthand for id="foo"; also #"foo bar", #`foo ${bar}`, #{expr}
  • .foo shorthand for class="foo" (but must be at least one space after tag name); also, ."foo bar", .`foo ${bar}`, .{expr}
    • "civet react" flag uses className instead of class
  • +foo shorthand for foo={true}, -foo/!foo shorthand for foo={false}
  • Any braced object literal can be used as an attribute: {foo}foo={foo}, {foo: bar}foo={bar}, {} remains as is; methods and getters/setters work too.
  • Attribute shorthand for {}
  • Attribute values without whitespace or suitably wrapped (parenthesized expressions, strings and template strings, regular expressions, array literals, braced object literals) do not need braces: foo=barfoo={bar}, count=count()count={count()}, sum=x+1sum={x+1}, list=[1, 2, 3]list={[1, 2, 3]}
  • Attributes can use computed property names: [expr]={value}{...{[expr]: value}}
  • "civet solid" flag adds correct types for JSX elements and fragments. Use "civet solid client" (default) for client-only code, "civet solid server" for server-only code (SSR only), or "civet solid client server" for isomorphic code that runs on client and server (SSR + hydration).
  • XML comments: <!-- ... -->{/* ... */}

TypeScript Enhancements

Changes from ES6

  • Implicit returns, even for multi-statement functions (avoid by specifying a void return type, adding a trailing ; or explicit return, or via the directive "civet -implicitReturns")
  • Disallow no parens on single argument arrow function. x => ... must become (x) => ... The reasoning is x -> ... => x(function() ...) in CoffeeScript and having -> and => behave more differently than they already do is bad. Passing an anonymous function to an application without parens is also convenient.
  • for(i of x) ... defaults to const declaration → for(const i of x) ...
  • Disallow comma operator in conditionals and many other places. if x, y is not allowed. But for i = 0, l = a.length; i < l; i++, i *= 2 is allowed.
  • Comma operator in case/when instead becomes multiple conditions.
  • Numbers can't end with a dot (otherwise would be ambiguous with CoffeeScript slices y[0..x]). This also implies that you can't access properties of numbers with 1..toString() use 1.toString() instead. When exponent follows a dot it is treated as a property access since an exponent could be a valid property 1.e101..e10. The workaround is to add a trailing zero 1.0e10 or remove the dot before the exponent 1e10.
  • Additional reserved words and, or, loop, until, unless
  • Experimental decorator syntax is @@ instead of @ because @ is premium real estate and, and @ is also static fields/methods, etc.
    class X
      method() {}
  • when inside switch automatically breaks and adds block scope.
  • else inside switch adds block scope.
  • No whitespace between unary operators and operands. Mandatory whitespace between condition and ternary ? ex. x ? a : b since x? is the unary existential operator.
  • No labels (yet...)

Scripting Improvements

  • Shebang line is kept unmodified in output
    console.log "hi"

Comparison to CoffeeScript

Take a look at this detailed Civet // CoffeeScript comparision

ECMAScript Compatibility

You can specify "civet" prologue directives to increase compatibility with ECMAScript/TypeScript:

Configuration What it enables
-implicit-returns turn off implicit return of last value in functions

Put them at the top of your file:

"civet -implicit-returns"

Your can separate multiple options with spaces.

Deno Compatibility

TypeScript only allows importing .ts files as .js. Deno follows ESM and requires importing files with the correct extension.

Civet automatically rewrites imports to work around this issue in TS.

When Civet detects it is running in Deno rewriting imports is turned off. If for some reason Civet fails to detect running in Deno you can turn off rewriting imports manually with these configuration options:

Configuration What it enables
-rewrite-ts-imports disable rewriting .ts -> .js in imports
deno currently just disables rewriting imports but could add more deno specific options in the future

Other Options

The "civet" prologue directive can also specify the following options:

Configuration What it enables
tab=NNN treat tab like NNN spaces (default=1)

For example, "civet tab=2" or "civet tab=4" lets you mix tabs and spaces in a file and be treated like they'd render in VSCode with editor.tabSize set accordingly.

Using Civet in your Node.js Environment

You have now been convinced that Civet is right for your current/next project. Here is how to set up your environment to get productive right away and have a Good Time℠.


Code coverage with c8 "just works" thanks to their source map integration and Civet's source maps.

Currently Civet's ESM loader depends on ts-node

c8 + Mocha


  "scripts": {
    "test": "c8 mocha",
  "c8": {
    "extension": [
  "mocha": {
    "extension": [
    "loader": [

ts-node must be configured with transpileOnly (it can't resolve alternative extensions). Also I think module needs to be at least ES2020 for the Civet ESM loader to work.


  "ts-node": {
    "transpileOnly": true,
    "compilerOptions": {
      "module": "ES2020"

If you don't care for code coverage you can skip c8 (but it is so easy why not keep it?).

You can also add .js and .ts extensions if you want to mix and match! Even .coffee will work if you require coffeescript/register or add a loader for it.

Execute the tests

yarn test

Step 4: Enjoy!


Use the alpha version of Civet Language Server

The language server provides syntax highlighting, completions, hover documentation, symbols outline, red squigglies, and go to definition.

Q? Why can't I just use the built-in VSCode TypeScript LSP?

A: VSCode's built in TypeScript LSP can't resolve non .ts/.js, not even with plugins. Maybe one day they'll allow for plugins that let you adjust the resolver and insert a transpilation step but until then a separate language server is necessary.

Q? Sometimes the file outline disappears and the red squigglies are all in the wrong place and maybe a notification pops up about some kind of LSP error.

A: I'm sorry that happened to you but the Civet Language Server is still alpha and improving rapidly. Please let me know exactly what happened and I'll try to do better next time.

It may happen when there is a syntax error in your Civet file. You can check and see if it compiles using the CLI tool in the meantime.

Please do submit bug reports / feature requests.


I strongly recommend using esbuild for building / packaging your Civet project.

import esbuild from 'esbuild'
import civetPlugin from '@danielx/civet/esbuild-plugin'{
  plugins: [
}).catch(() => process.exit(1))

It's super fast and works great!


Civet is a large language that feels small. Civet is large because it is mostly a superset of TypeScript, an already large language. Civet feels small because of the coherent design aesthetic: related features look and behave similarly, so when seeing a new feature you can have a good idea what it does, and your existing knowledge of JavaScript and other languages leads you in the right direction.

Civet works with existing tools. We're not trying to replace the TypeScript type checker; we want to amplify its power. We're not trying to change ES semantics; we want to present them in a coherent and expressive way.

Less syntax is preferred.

Context matters. The same tokens can mean different things in different contexts. This shouldn't be arbitrary but based on pragmatic concerns. Things should be consistent where possible, especially conceptually.

Civet builds on top of history. We've taken inspiration from languages like CoffeeScript, Elm, LiveScript, Flow, Haskell, Perl, Python, Ruby, Crystal, Bash, and others.

Civet is pragmatic. Civet design is informed by 25+ years of JavaScript development. Frontend frameworks have come and gone but they all addressed issues that were important for their time. We focus heavily on addressing concerns that real developers feel every day. A key criteria for evaluating features is "how does it work in practice?".

Civet evolves. As the official JS and TS specifications evolve into the future, Civet also evolves favoring compatibility. This may lead us to difficult choices where the future spec has evolved differently than we anticipated (pipe operators, do expressions, pattern matching). In those cases, Civet will adapt to match the latest spec while providing configuration options to allow migration bit by bit while keeping existing code working.

Civet is configurable. There is no single "right way" for everyone at all times. Some of us have older CoffeeScript codebases that would benefit from added types. Others have massive TypeScript applications that could benefit from new language features and shorthand syntax. Civet provides a way to get the benefits bit by bit without a complete rewrite. This same configurability lets us experiment with language features to gain experience and improve them before locking them in. It also allows us to adapt to a changing future.


If you are so inclined, you can sponsor Civet on Open Collective.