TypedScript: Imagining CoffeeScript with Types

The content envisions a hypothetical programming language called “TypedScript,” merging the elegance of CoffeeScript with TypeScript’s type safety. It advocates for optional types, clean syntax, aggressive type inference, and elegance in generics, while maintaining CoffeeScript’s aesthetic. The idea remains theoretical, noting practical challenges with adoption in the current ecosystem.

After writing my love letter to CoffeeScript, I couldn’t stop thinking: what if CoffeeScript had embraced types instead of fading away? What if someone had built a typed version that kept all the syntactic elegance while adding the type safety that makes TypeScript so powerful?

Let’s imagine that world. Let’s design what I’ll call “TypedScript” (or maybe CoffeeType? TypedCoffee? We’ll workshop the name). The goal: keep everything that made CoffeeScript beautiful while adding first-class support for types and generics.

The Core Principles

Before we dive into syntax, let’s establish what we’re trying to achieve:

  1. Types should be optional but encouraged. You can write untyped code and gradually add types.
  2. Syntax should stay clean. No angle brackets everywhere, no visual noise.
  3. Type inference should be aggressive. The compiler should figure out as much as possible.
  4. Generics should be elegant. No <T, U, V> mess.
  5. The Ruby/Python aesthetic must be preserved. Significant whitespace, minimal punctuation, readable code.

Basic Type Annotations

Let’s start simple. In TypeScript, you write:

const name: string = "Ivan";
const age: number = 30;
const isActive: boolean = true;
Code language: JavaScript (javascript)

In TypedScript, I’d imagine:

name: String = "Ivan"
age: Number = 30
isActive: Boolean = true
Code language: JavaScript (javascript)

Or with type inference (which should work most of the time):

name = "Ivan"        # inferred as String
age = 30             # inferred as Number
isActive = true      # inferred as Boolean
Code language: PHP (php)

The colon for type annotations feels natural. It’s what TypeScript uses, and it doesn’t clash with CoffeeScript’s existing syntax.

Function Signatures

TypeScript function types can get verbose:

function greet(name: string, age?: number): string {
  return age 
    ? `Hello ${name}, you are ${age}` 
    : `Hello ${name}`;
}

const add = (a: number, b: number): number => a + b;
Code language: JavaScript (javascript)

TypedScript could look like this:

greet = (name: String, age?: Number) -> String
  if age?
    "Hello #{name}, you are #{age}"
  else
    "Hello #{name}"

add = (a: Number, b: Number) -> Number
  a + b
Code language: JavaScript (javascript)

Even cleaner with inference:

greet = (name: String, age?: Number) ->
  if age?
    "Hello #{name}, you are #{age}"
  else
    "Hello #{name}"

add = (a: Number, b: Number) -> a + b
Code language: JavaScript (javascript)

The return type is inferred from the actual return value. This is already how CoffeeScript works (implicit returns), so we just layer types on top.

Interfaces and Type Definitions

TypeScript interfaces are pretty clean, but they still require curly braces:

interface User {
  id: string;
  name: string;
  email: string;
  age?: number;
  roles: string[];
}
Code language: PHP (php)

In TypedScript, we could use indentation:

type User
  id: String
  name: String
  email: String
  age?: Number
  roles: [String]
Code language: JavaScript (javascript)

Or for inline types:

user: {id: String, name: String, email: String}
Code language: CSS (css)

Arrays could use the Ruby-inspired [Type] syntax. Tuples could be [String, Number]. Maps could be {String: User}.

Classes with Types

TypeScript classes are already pretty good, but they’re still verbose:

class UserService {
  private users: User[] = [];
  
  constructor(private apiClient: ApiClient) {}
  
  async getUser(id: string): Promise<User> {
    const response = await this.apiClient.get(`/users/${id}`);
    return response.data;
  }
  
  addUser(user: User): void {
    this.users.push(user);
  }
}
Code language: JavaScript (javascript)

TypedScript version:

class UserService
  users: [User] = []
  
  constructor: (@apiClient: ApiClient) ->
  
  getUser: (id: String) -> Promise<User>
    response = await @apiClient.get "/users/#{id}"
    response.data
  
  addUser: (user: User) -> Void
    @users.push user
Code language: HTML, XML (xml)

The @ syntax for instance variables is preserved, and we just add type annotations where needed. Constructor parameter properties (@apiClient: ApiClient) combine declaration and assignment in one elegant line.

Generics: The Tricky Part

This is where TypeScript gets ugly. Generics in TypeScript look like this:

class Container<T> {
  private value: T;
  
  constructor(value: T) {
    this.value = value;
  }
  
  map<U>(fn: (value: T) => U): Container<U> {
    return new Container(fn(this.value));
  }
}

function identity<T>(value: T): T {
  return value;
}

const result = identity<string>("hello");
Code language: JavaScript (javascript)

The angle brackets are noisy, and they clash with comparison operators. TypedScript needs a different approach. What if we used a more natural syntax inspired by mathematical notation?

class Container of T
  value: T
  
  constructor: (@value: T) ->
  
  map: (fn: (T) -> U) -> Container of U for any U
    new Container fn(@value)

identity = (value: T) -> T for any T
  value

result = identity "hello"  # type inferred

The of keyword introduces type parameters for classes. The for any T suffix introduces type parameters for functions. When calling generic functions, types are inferred automatically in most cases.

For multiple type parameters:

class Pair of K, V
  constructor: (@key: K, @value: V) ->
  
  map: (fn: (V) -> U) -> Pair of K, U for any U
    new Pair @key, fn(@value)

Union Types and Intersections

TypeScript uses | for unions and & for intersections:

type Result = Success | Error;
type Employee = Person & Worker;
Code language: JavaScript (javascript)

TypedScript could keep this, but make it more readable:

type Result = Success | Error

type Employee = Person & Worker

# Or with more complex types
type Response = 
  | {status: "success", data: User}
  | {status: "error", message: String}
Code language: PHP (php)

Advanced Generic Constraints

TypeScript has complex generic constraints:

function findMax<T extends Comparable>(items: T[]): T {
  return items.reduce((max, item) => 
    item.compareTo(max) > 0 ? item : max
  );
}
Code language: JavaScript (javascript)

In TypedScript:

findMax = (items: [T]) -> T for any T extends Comparable
  items.reduce (max, item) ->
    if item.compareTo(max) > 0 then item else max
Code language: JavaScript (javascript)

Practical Example: Building a Generic Repository

Let’s build something real. Here’s a TypeScript generic repository:

interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  save(entity: T): Promise<T>;
  delete(id: string): Promise<void>;
}

class ApiRepository<T> implements Repository<T> {
  constructor(
    private endpoint: string,
    private client: HttpClient
  ) {}
  
  async findById(id: string): Promise<T | null> {
    try {
      const response = await this.client.get(`${this.endpoint}/${id}`);
      return response.data;
    } catch (error) {
      return null;
    }
  }
  
  async findAll(): Promise<T[]> {
    const response = await this.client.get(this.endpoint);
    return response.data;
  }
  
  async save(entity: T): Promise<T> {
    const response = await this.client.post(this.endpoint, entity);
    return response.data;
  }
  
  async delete(id: string): Promise<void> {
    await this.client.delete(`${this.endpoint}/${id}`);
  }
}
Code language: JavaScript (javascript)

The TypedScript version:

interface Repository of T
  findById: (id: String) -> Promise<T?>
  findAll: () -> Promise<[T]>
  save: (entity: T) -> Promise<T>
  delete: (id: String) -> Promise<Void>

class ApiRepository of T implements Repository of T
  constructor: (@endpoint: String, @client: HttpClient) ->
  
  findById: (id: String) -> Promise<T?>
    try
      response = await @client.get "#{@endpoint}/#{id}"
      response.data
    catch error
      null
  
  findAll: () -> Promise<[T]>
    response = await @client.get @endpoint
    response.data
  
  save: (entity: T) -> Promise<T>
    response = await @client.post @endpoint, entity
    response.data
  
  delete: (id: String) -> Promise<Void>
    await @client.delete "#{@endpoint}/#{id}"

# Usage
userRepo = new ApiRepository of User "users", httpClient
users = await userRepo.findAll()
Code language: HTML, XML (xml)

Look at how clean that is. No angle brackets, no semicolons, no excessive braces. The type information is there, but it doesn’t dominate the code.

Type Guards and Narrowing

TypeScript’s type guards work well:

function isString(value: unknown): value is string {
  return typeof value === "string";
}

if (isString(data)) {
  console.log(data.toUpperCase());
}
Code language: JavaScript (javascript)

TypedScript could use a similar pattern:

isString = (value: Unknown) -> value is String
  typeof value == "string"

if isString data
  console.log data.toUpperCase()
Code language: JavaScript (javascript)

Utility Types

TypeScript has utility types like Partial<T>, Pick<T, K>, Omit<T, K>. These could work in TypedScript with a more natural syntax:

# TypeScript
type PartialUser = Partial<User>;
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;

# TypedScript
type PartialUser = Partial of User
type UserPreview = Pick of User, "id" | "name"
type UserWithoutEmail = Omit of User, "email"
Code language: PHP (php)

The Existential Operator with Types

Remember CoffeeScript’s beloved ? operator? It would work beautifully with nullable types:

user: User? = await findUser id  # User | null

name = user?.name ? "Guest"
user?.profile?.update()
callback?()
Code language: PHP (php)

The ? in User? means nullable, just like TypeScript’s User | null or User | undefined.

Real-World Example: A Todo App

Let’s put it all together with a realistic example:

type Todo
  id: String
  title: String
  completed: Boolean
  createdAt: Date

type TodoFilter = "all" | "active" | "completed"

class TodoStore
  todos: [Todo] = []
  filter: TodoFilter = "all"
  
  constructor: (@storage: Storage) ->
    @loadTodos()
  
  loadTodos: () -> Void
    data = @storage.get "todos"
    @todos = if data? then JSON.parse data else []
  
  saveTodos: () -> Void
    @storage.set "todos", JSON.stringify @todos
  
  addTodo: (title: String) -> Todo
    todo: Todo =
      id: generateId()
      title: title
      completed: false
      createdAt: new Date()
    
    @todos.push todo
    @saveTodos()
    todo
  
  toggleTodo: (id: String) -> Boolean
    todo = @todos.find (t) -> t.id == id
    return false unless todo?
    
    todo.completed = !todo.completed
    @saveTodos()
    true
  
  deleteTodo: (id: String) -> Boolean
    index = @todos.findIndex (t) -> t.id == id
    return false if index == -1
    
    @todos.splice index, 1
    @saveTodos()
    true
  
  getFilteredTodos: () -> [Todo]
    switch @filter
      when "active" then @todos.filter (t) -> !t.completed
      when "completed" then @todos.filter (t) -> t.completed
      else @todos

generateId = () -> String
  Math.random().toString(36).substr 2, 9

Compare that to the TypeScript equivalent and tell me it isn’t more elegant. The types are there, providing safety and documentation, but they don’t overwhelm the code. You can still read it naturally.

Why This Matters

TypeScript won because it added types to JavaScript without fundamentally changing the language. That was smart from an adoption standpoint. But it meant keeping JavaScript’s verbose syntax.

If TypedScript had existed, we could have had both: the elegance of CoffeeScript and the safety of TypeScript. We could write code that’s both beautiful and robust.

The tragedy is that this never happened. CoffeeScript’s creator, Jeremy Ashkenas, explicitly rejected adding types. He felt they went against CoffeeScript’s philosophy of simplicity. Meanwhile, TypeScript embraced JavaScript’s syntax for compatibility.

Could This Still Happen?

Technically, someone could build this. The CoffeeScript compiler is open source. TypeScript’s type system is well-documented. A sufficiently motivated team could fork CoffeeScript and add a type system.

But would anyone use it? Probably not. The JavaScript ecosystem has moved on. TypeScript has won. The tooling, the community, the momentum are all there. Starting a new compile-to-JavaScript language in 2025 would be fighting an uphill battle.

Still, it’s fun to imagine. And who knows? Maybe in some parallel universe, TypedScript is the dominant language for web development, and developers there are writing beautiful, type-safe code that makes our TypeScript look verbose and clunky.

A developer can dream.

The Syntax Reference

For anyone curious, here’s a quick reference of what TypedScript syntax could look like:

# Basic types
name: String = "Ivan"
age: Number = 30
active: Boolean = true
data: Any = anything()
nothing: Void = undefined

# Arrays and tuples
numbers: [Number] = [1, 2, 3]
tuple: [String, Number] = ["Ivan", 30]

# Objects
user: {name: String, age: Number} = {name: "Ivan", age: 30}

# Nullable types
optional: String? = null

# Union types
status: "pending" | "active" | "complete" = "pending"
value: String | Number = 42

# Functions
greet: (name: String) -> String = (name) -> "Hello #{name}"

# Generic functions
identity = (value: T) -> T for any T
  value

# Generic classes
class Container of T
  value: T
  constructor: (@value: T) ->

# Interfaces
interface Comparable of T
  compareTo: (other: T) -> Number

# Type aliases
type UserId = String
type Result of T = {ok: true, value: T} | {ok: false, error: String}

# Constraints
sorted = (items: [T]) -> [T] for any T extends Comparable of T
  items.sort (a, b) -> a.compareTo b

Closing Thoughts

Would TypedScript be better than TypeScript? For me, yes. The cleaner syntax, the Ruby-inspired aesthetics, the focus on readability, all while keeping the benefits of static typing. It would be the best of both worlds.

But “better” is subjective. TypeScript’s compatibility with JavaScript is a huge advantage. Its massive ecosystem is irreplaceable. Its tooling is mature and battle-tested.

TypedScript would be a beautiful language that few people use. And maybe that’s okay. Not every good idea wins. Sometimes the practical choice beats the elegant one.

But I still wish I could write my production code in TypedScript. I think it would be a joy.

What do you think? Would you use TypedScript if it existed? What syntax choices would you make differently? Let me know in the comments.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.