Paris Typescript Conference Report - Morning

The TypeScript community gathered in Paris for the first time for a day of conferences dedicated to the language. Covering best practices, emerging tools, and valuable insights, this edition offered an opportunity to explore a wide range of topics.

In this report, we want to give you the key ideas shared by the morning’s speakers.

Paris Typescript introduction photo with BeJS who helped organising the event

TypeScript’s Teenage Years

Josh Goldberg - Open Source Developer, TypeScript- ESLint

TypeScript, the popular programming language that brings status typing to JavaScript, is 13 years old now, which is equivalent to a teenager.

Josh started his talk by a brief introduction of how TypeScript was born, He referenced the TypeScript Documentary to learn more about its origin.

He talked about the several hard-learned lessons during its first decade:

  • No runtime syntax extensions: unlike some languages that evolve with runtime extensions, TypeScript didn’t choose to introduce them.
  • Monorepo management: the importance of managing monorepose effectively has been a major takeaway for large-scale TypeScript projects.
  • The lack of good training: while TypeScript has become immensely popular, the ecosystem still struggles with a lack of high-quality, standardized training materials.

Josh pointed after that to some good projects of TypeScript such as:

  • Knip: Tools for detecting unused files and dependencies in TypeScript projects.
  • Recreate DOOM by using TypeScript.

He talked also about some lessons extended beyond TypeScript itself that serve as valuable advice for open-source projects and developers:

  • Understand your ecosystem: A deep understanding of your project( tools, libraries and frameworks).
  • Do what’s right, not what’s easy: Prioritize long-term benefits over short-term convenience.
  • Play to your strength: Focus on what your project does best and avoid unnecessary scope.
  • Don’t overreach (unless it’s really worth it): Avoid unnecessary complexity unless it brings significant benefits.
  • Measure twice, cut once: Plan carefully before making major changes.
  • Let the team cook: Trust the expertise of the contributors.
  • Community is key: A strong, engaged community is essential for any open-source project’s success.
  • Craft the message
  • Aggregate, distill, extract
  • Embrace the platform
  • Support great tooling
  • Enjoy the ride: Open-source development is a journey so enjoy it.

Finally, Josh shared an interesting technique. When Node.js transpiles TypeScript into JavaScript, it doesn’t just strip away the types, instead it replaces them with whitespace. This ensures that the source maps retain the correct origin positions and makes debugging easier.

Type safety in your CLIs

Maël Nison - Staff Software Engineer, Datadog

Maël started his talk about best practices for building robust CLIs by leveraging TypeScripts’s powerful type to prevent runtime errors and improve developer experience.

He compared different command line arguments tools and how we can enforce them and make them strongly typed.

He mentioned Commander and Yargs but he is working on Clipanion library, which is inspired by Commander, supports more syntaxes and generates a formal state machine.

An example of using Clipanion library to create simple CLI tool:

import {Command, Option, runExit} from 'clipanion';<br>runExit([<br> class AddCommand extends Command {<br>   static paths = [[`add`]];
   name = Option.String();
   async execute() {
     this.context.stdout.write(`Adding ${this.name}!\n`);
   }
 },

 class RemoveCommand extends Command {
   static paths = [[`remove`]];
   name = Option.String();
   async execute() {
     this.context.stdout.write(`Removing ${this.name}!\n`);
   }
 },
]);

We then discovered how we can do React interfaces in the CLI. The original way was to use Ink. Maël created a new project Terminosaurus. It offers advanced features such as:

  • Type safety
  • Integration with React
  • Support CSS
  • Allows Advanced Layouts: supports flexbox layouts, and also relative and absolute positioning, overflow, scrolling, and more, which are not handled by Ink.

Example of using Terminosaurus:

import {useState} from 'react';
import {render} from 'terminosaurus/react';
function App() {
  const [counter, setCounter] = useState(0);

  return (
    <term:div onClick={() => setCounter(counter + 1)}>
      Counter: {counter}
    </term:div>
  );
}
render({}, <App/>);

We learned that by implementing strong typing and utilizing tools such as Clipanion and Terminosaurus, developers can create more reliable, maintainable and user-friendly CLI tools.
Maël’s insights offer valuable strategies for integrating type safety into workflows, whether working on simple script or complex developer tools.

Fullstack form validation with Zod

Elise Patrikainen - Freelance front-end developer

How can we create a form that has data type validation, dynamic options and can be validated on the front-end and backend?

The form data can be validated using a validation library. Nowadays we can use Zod to help with data validation in Typescript.

We could also use:

  • Yup, but has less built-in schemas
  • Joi, but there is no static-type inference
  • Io-ts, very functional
  • Vine, but is node only

We want to handle dynamic form options on the front-end. The next form input displayed depends on the previously selected option. For example, if you choose that you had Covid, we should display a list of Covid-related symptoms.

We want the front-end to display this, but also our backend to correctly validate the data. We want to make sure that the front-end and back-end validation are the same.

To do that, Elise suggests that we create a meta-schema that describes the form and the different “path” of options it can take.

This logic is mutualised between the frontend and the backend. This ensures that there are no differences between what the frontend displays and what the backend expects.

This schema helps with better separation of concerns because the form logic is decoupled from the rest of the code.

We also use it to generate the zod validation schema.

The front-end uses a component that understands the meta-schema and generates a form from it. We now have a generic frontend form feature.

An example of schema:

export const covidSchema: Schema[] = [
  {
    legend: 'What is the main symptom?',
    _key: 'covidSymptoms',
    options: [{
      value: 'fever',
      label: 'Fever',
    },{
      value: 'chills',
      label: 'Chills',
    },{
      value: 'sore_throat',
      label: 'Sore throat',
    }],
    rules: z.union([z.literal("fever"), z.literal("chills"), z.literal("sore_throat")]),
    type: 'radio',
  },
  {
    legend: 'Do you have difficulty beathing?',
    _key: 'respiration',
    options: [{
      value: true,
      label: 'Yes',
    }, {
      value: false,
      label: 'No',
    }],
    rules: z.boolean(),
    type: 'radio',
  }
];


export const gastroSchema: Schema[] = [
  {
    legend: 'What is the main symptom?',
    _key: 'gastroSymptoms',
    options: [{
      value: 'diarrhea',
      label: 'Diarrhea',
    },{
      value: 'nausea',
      label: 'Nausea',
    },{
      value: 'fever',
      label: 'Fever',
    }],
    rules: z.union([z.literal("diarrhea"), z.literal("nausea"), z.literal("fever")]),
    type: 'radio',
  },
];


export function schemaComputer(formData: FormDataObject) {
  let diseaseSchema: Schema[] = []
  switch (formData.disease) {
    case 'covid': diseaseSchema = [...covidSchema]; break;
    case 'gastro': diseaseSchema = [...gastroSchema]; break;
  }
  return [...basisSchema, ...diseaseSchema]
}

You can see more on Elise’s code example repository.

This proof of concept demonstrates a powerful approach to unifying front-end and back-end validation while keeping the form logic flexible and dynamic.

However, we found that it has some limitations:

  • Where do we put the limit? It works for simple forms. For more complicated ones, we could end up adding “front-end” properties to the meta-schema so that the generic front-end component correctly displays the form. Those properties are closely related to the front-end design system, and should not exist on this meta-schema that wants to be agnostic.
  • The current implementation does not have a single zod schema. The schema is dynamically built using the meta-schema and the selected options. For example the schema will contain the covid options only if covid is selected. This means that the zod schema is not fixed. We cannot use the z.infer method to infer the type from the schema.

Mastering TypeScript for Real-World Applications

Nicolas Dubien - Principal Software Engineer, Pigment

Nicolas presented a bunch of TypesScript tips to use for a real world application.

Ban any from your codebase

This is a bad practice. It can compromise type safety. Typescript cannot enforce type-related check or validation. This can lead to runtime errors. It also impacts code quality, making it more difficult to maintain because we do not have information about the type of the variable.

Typescript will not be able to check for future-breaking changes that may occur due to updates in the codebase.

This also makes debugging more difficult.

Sometimes we do not know the type yet. What I am doing in those cases is defining a global type TODO that I use instead of any.

global {
  type TODO = any
}

By using TODO instead of any, we are signaling to other developers that the type should be changed. It serves as a kind of "doc comment" for the type, making it clear that it's not yet finalized.

Branded Types

Branded Types enhance type safety. It is a way to create unique data beyond primitive types.

For example:

const x: number = 0;
const y: number = 0;

We do not have strict typing on x and y. Both are numbers. We could send the x variable to a function that expects an y.

To fix that, we attach a unique “brand” to our primitive type using a unique symbol

declare const validX: unique symbol;
type X = number & { [validX]: true };

declare const validY: unique symbol;
type Y = number & { [validY]: true };
  • It helps type safety
  • It makes our code more readable (with use a type Y instead of a number)
  • It prevent errors on the code (with cannot pass an Y parameter instead of an X)

Effect library has its own implementation of branded types.

Discriminated Unions

You can use a typescript literal type (a javascript primitive value) such as a string as a property to discriminate between union members.

interface Square {
  kind: "square";
  size: number;
}

interface Rectangle {
  kind: "rectangle";
  width: number;
  height: number;
}
type Shape = Square | Rectangle;

When you are using a comparison operator or a switch checking for the discriminant property (kind), typescript will be able to deduce the real type of your object

if (shape.kind === "square") {
  // Typescript deduces that the shape is a Square.
  // We can use the Square properties	
  return shape.size * shape.size;
}

Using a switch you can use a default as:

function assertInvalidValue(x:never): never {
  throw new Error('Unexpected value');
}

switch (shape.kind) {
  case "square": return shape.size * shape.size;
  case "rectangle": return shape.width * shape.height;
  // gives a compilation error if a new type is added and will throw if an invalid value is given at runtime
  default: return assertInvalidValue(shape);
}

Type predicates

“A type guard is some expression that performs a runtime check that guarantees the type in some scope.”

This is a function whose return type is a type predicate. In this example, the predicate is pet is Cow.

function isCow(pet: Cow | Cat): pet is Cow {
  return (pet as Cow).moo !== undefined;
}

When we call isFish and it returns true, Typescript will narrow the variable to the Cow type.

if (isCow(pet)) {
  pet.moo();
}

Another example

function isString(x: any): x is string {
  return typeof x === "string";
}

function isNumber(x: any): x is number {
  return typeof x === "number";
}

Mapped types

Mapped types are useful when a type is based on another type and we do not want to repeat ourselves.

Learn more about it on the Typescript documentation

Never

Make your code future proof with this small utility function that assert when it is called.

export function assertUnreachable<T>(arg: never, defaultValue: T) {
  return defaultValue;
}

return value.type === 'number'
  ? String(value.numberValue)
  : value.type === 'text'
  ? String(value.textValue);
  : assertUnreachable(value.type, "Not supported yet");

Generic and variadics

Generics exist in a lot of programming languages. Learn about Typescript generics on the official documentation.

API first design and execution with TypeSpec

Natalia Venditto

Natalia introduced us to TypeSpec, a powerful tool that helps developers create and share API structures and rules efficiently. It is designed to tackle common challenges in API development, such as versioning, patterns, spec authoring, multi-protocol support, extensibility, and diagnostics.

TypeSpec is a tool that allows developers to describe APIs in a structured and type-safe way. It works with multiple API styles, including RESTful APIs, GraphQL, and Protocol Buffers. By using TypeSpec, developers can:

  • Ensure Type Safety – Prevent inconsistencies in API structures.
  • Support Multiple API Styles – Work seamlessly with REST, GraphQL, and Protocol Buffers.
  • Standardize API Definitions – Make API contracts clear and easy to share.
  • Generate API Schemas & Code – Automatically create schemas, API specifications, client/server code, documentation, and more.

Example: OpenAPI and JSON Schema

TypeSpec integrates with OpenAPI specifications and JSON Schema, ensuring that APIs remain type-safe and consistent across different platforms.

Using its own language, TypeSpec allows developers to define APIs efficiently while benefiting from a complete tooling ecosystem, including a VS Code extension for better development experience.

Want to see TypeSpec in action? Try it here: TypeSpec Playground

TypeSpec is designed to improve API-first development by helping build reliable and well-structured APIs!

Using Effect to build Production-Ready TypeScript Software

Antoine Coulon

When developing TypeScript applications, handling errors, managing resources, and maintaining clean code can be challenging. TypeScript has a large ecosystem of libraries, but this ecosystem is often fragmented—dependencies within a project are not necessarily designed to work together, adding complexity to development.

Antoine introduced us to Effect, a TypeScript library that fully leverages TypeScript’s capabilities to simplify these challenges. Effect provides a unified approach to resilience, testing, composability, concurrency, resource management, and monitoring essentially everything needed to build production-ready applications.

What is Effect?

Effect defines a generic datatype that describes a program with three type parameters:

  • A (Success): The expected result of the program.
  • E (Error): The possible error that might occur.
  • R (Requirements): The environment needed to execute the program.

This can be represented as:

Effect<A, E, R>;

Here’s a simple example of defining a program using Effect:

import type { Effect } from "effect";

type Program<Environment, Error, Success> = Effect.Effect<
  Success,
  Error,
  Environment>;

Why Use Effect?

Effect offers several advantages for TypeScript development:

  • Better Error Handling – Clearly defines and manages expected errors.
  • Improved Testability – Makes it easier to write unit tests with predictable behaviors.
  • Structured Code – Encourages maintainable and composable programming.
  • Resource Management – Helps manage dependencies and system resources efficiently.

Solving Common Development Challenges

Effect is designed to address key issues in building robust applications:

  • Resilience – Handles failures gracefully.
  • Dependencies – Manages shared dependencies across modules.
  • Concurrency – Overcomes TypeScript promises' limitations, including:
    • No interruption handling
    • No guaranteed resource release
    • No control over execution
  • Resource Management – Ensures proper handling of interruptions and errors.
  • Observability – Provides built-in logging, tracing, and metrics.

Effect as a Complete Ecosystem

Effect is more than just a library—it’s an ecosystem of powerful tools, including:

  • Batching & caching
  • Streams
  • Schema validation
  • Configuration management
  • Data structures
  • State management
  • Scheduling

Learn More

If you're looking for a powerful, type-safe, and production-ready solution for TypeScript applications, Effect is worth exploring. You can try it out, experiment with examples, or take a crash course.

For more information : https://github.com/antoine-coulon/effect-introduction

Conclusion

The first half day of the conference was rich in insights. This inaugural edition of Paris TypeScript La Conf’ was a great success, bringing together the community to share best practices, explore new tools, and exchange experiences.