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.
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:
Josh pointed after that to some good projects of TypeScript such as:
He talked also about some lessons extended beyond TypeScript itself that serve as valuable advice for open-source projects and developers:
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.
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:
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.
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:
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:
Nicolas Dubien - Principal Software Engineer, Pigment
Nicolas presented a bunch of TypesScript tips to use for a real world application.
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 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 };
Effect library has its own implementation of branded types.
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);
}
“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 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
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");
Generics exist in a lot of programming languages. Learn about Typescript generics on the official documentation.
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:
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!
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.
Effect defines a generic datatype that describes a program with three type parameters:
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>;
Effect offers several advantages for TypeScript development:
Effect is designed to address key issues in building robust applications:
Effect is more than just a library—it’s an ecosystem of powerful tools, including:
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
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.