Gérer la complexité avec les types opaques

Sébastien BESNIER

Sébastien BESNIER

Code

Consultante

Produit les rapports

Consulte les rapports

Facteur d'émission

Combien de kg de CO2 pour 1kg de produit ?

Quel est le plus émetteur ?

Banane sous plastique  VS steak de boeuf local ?

Facteur d'émission

TOTAL : 1.09 kgCO2e/kg

0.29523642* kgCO2e/ kg

Agriculture

0.45139732* kgCO2e/ kg

Transport

0.35** kgCO2e/ kg

Emballage

TOTAL : 1.09 kgCO2e/kg

34.7032563* kgCO2e/ kg

Agriculture

34.7032563* kgCO2e/ kg

Agriculture

* : source Agribalyse 3.2 Tableur produits alimentaires VF 20_11_24

** : Estimation avec un sachet plastique de 10 g pour une banane de 70 g (sans la peau) en comptant 2.5KgCO2e pour 1kg de plastique

34.7032563 kgCO2e/ kg

Agriculture

34.7032563.toPrecision(4) === "34.70"
Number(34.7032563.toPrecision(4)) === 34.70
34.7032563.toPrecision(4) === "34.70"
  • Comment trouver tous les usages ?
  • Que les prochains développeurs ne cassent pas cette règle ?
  • Comment trouver tous les usages ?
  • Que les prochains développeurs ne cassent pas cette règle ?

Un type
FourDigits
pour les facteurs d'émission

non assignable à number

const Computation = (props: Data) => {
  const total = props.quantity * props.emissionFactor;
  return <div>
    <div>{total}</div>
    <EF emissionFactor={props.emissionFactor} />
  </div>;
}


const EF = (props: {emissionFactor: number})=>
  <div>{emissionFactor}</div>;

AVANT

Type number

const Computation = (props: Data) => {
  const total = props.quantity * props.emissionFactor;
  return <div>
    <div>{total}</div>
    <EF emissionFactor={props.emissionFactor} />
  </div>;
}


const EF = (props: {emissionFactor: number})=>
  <div>{emissionFactor}</div>;

APRÈS

Type FourDigits

Cannot multiply by FourDigits

FourDigits is not number

const Computation = (props: Data) => {
  const total = props.quantity * toNumber(
    props.emissionFactor
  );
  return <div>
    <div>{total}</div>
    <EF emissionFactor={props.emissionFactor} />
  </div>;
}


const EF = (props: {emissionFactor: FourDigits})=>
  <div>{emissionFactor}</div>;

APRÈS

toNumber: (v: FourDigits) => number

Cannot display FourDigits

const Computation = (props: Data) => {
  const total = props.quantity * toNumber(
    props.emissionFactor
  );
  return <div>
    <div>{total}</div>
    <EF emissionFactor={props.emissionFactor} />
  </div>;
}


const EF = (props: {emissionFactor: FourDigits})=>
  <div>{display(emissionFactor)}</div>;

APRÈS

display: (v: FourDigits) => string

toNumber: (v: FourDigits) => number

  • Comment trouver tous les usages ?
  • Que les prochains développeurs ne cassent pas cette règle ?

Un type
FourDigits
pour les facteurs d'émission

non assignable à number

declare const brand: unique symbol;
export type FourDigits = { [brand]: "FourDigits"};

export const toFourDigits = (n: number) =>
  n as FourDigits;

export const toNumber = (v: FourDigits) =>
  Number((v as number).toPrecision(4));

export const display = (v: FourDigits) => 
  (v as number).toPrecision(4);

Symbole non exporté, impossible à construire en dehors du module

`as` sur FourDigits uniquement dans ce module

fourDigits.ts
 

Navigateur

Serveur

HTTP

NodeJS

React

tRPC

Les types sont partagés !

NodeJS

React

tRPC

Les types sont partagés !

Validation des données avec Zod

// ...

export const toFourDigits = (n: number) =>
  n as FourDigits;

const zFourDigits = z
	.number()
	.transform(toFourDigits) as ZodType<FourDigits, 
                                        FourDigits>;

fourDigits.ts
 

Navigateur

Serveur

NodeJS

React

tRPC

Les types sont partagés !

Navigateur

Serveur

NodeJS

React

BDD

tRPC

Postgresql

Prisma

NodeJS

Postgresql

Prisma

model Report {
  // ...
  emissionFactor Float 
  // ...
}

schema.prisma
 

CREATE TABLE (
   -- ...
   emissionFactor 
      DOUBLE PRECISION 
      NOT NULL,
   -- ...
)

SQL
 

const r = report.findFirst({
  emissionFactor: true  
});

r.emissionFactor: number

server.ts
 

Client.ts

NodeJS

Postgresql

Prisma

model Report {
  // ...
  emissionFactor Float /// type: FourDigits
  // ...
}

schema.prisma
 

CREATE TABLE (
   -- ...
   emissionFactor 
      DOUBLE PRECISION 
      NOT NULL,
   -- ...
)

SQL
 

const r = report.findFirst({
  emissionFactor: true  
});

r.emissionFactor: FourDigits

server.ts
 

CustomClient.ts

Navigateur

Serveur

NodeJS

React

BDD

tRPC

Postgresql

Prisma

(modifié)

FourDigits

FourDigits

FourDigits

Markdown

**texte** affiché au lieu de texte

Aucune indication que le texte en cours d'édition va être affiché comme du Markdown

Markdown

declare const brand: unique symbol;
export type MarkdownString = { [brand]: "Markdown"};

export const toMarkdown = (n: string) =>
  n as MarkdownString;

export const Markdown = (props: {
  children: MarkdownString
}): ReactNode => {/*...*/}

export cont MarkdownEditor = (props: {
  input: MarkdownString;
  setInput: (input: MarkdownString)=> void;
}): ReactNode => {/*...*/ }

markdown.ts
 

Uuid

declare const brand: unique symbol;
export type Uuid = { [brand]: "Uuid"};

uuid.ts
 

userUuid === userName

Cannot compare Uuid and string

userUuid === companyUuid

Uuid<Entity>

userUuid === userName

Cannot compare Uuid<"User"> and string

userUuid === companyUuid
declare const brand: unique symbol;
export type Uuid<Entity> = { [brand]: Entity };

uuid.ts
 

Cannot compare Uuid<"User"> and Uuid<"Company">

NodeJS

Postgresql

Prisma

model Report {
  // ...
  emissionFactor Float /// type: FourDigits
  // ...
}

schema.prisma
 

CREATE TABLE (
   -- ...
   emissionFactor 
      DOUBLE PRECISION 
      NOT NULL,
   -- ...
)

SQL
 

const r = report.findMany({
  emissionFactor: true  
});

r.emissionFactor: FourDigits

server.ts
 

CustomClient.ts

NodeJS

Postgresql

Prisma

model Report {
  // ...
  emissionFactor Float /// type: FourDigits
  // ...
}

schema.prisma
 

CREATE TABLE (
   -- ...
   emissionFactor 
      DOUBLE PRECISION 
      NOT NULL,
   -- ...
)

SQL
 

const r = report.findMany({
  emissionFactor: true  
});

r.emissionFactor: FourDigits

server.ts
 

CustomClient.ts

  • La clef primaire de la table User est Uuid<"User">
  • La clef étrangère companyUuid est Uuid<"Company">
  • Champs typés à la création et modification et dans les clauses comme
    where: { companyUuid: input }

Navigateur

Serveur

NodeJS

React

BDD

tRPC

Postgresql

Prisma

(modifié)

Uuid<Entity>

Uuid<Entity>

Uuid<Entity>

Résumé

  • Boeuf 30 fois plus émissif que végétal
  • Type opaque : seul le module le définissant peut le manipuler
  • Typage "bout à bout" : de la BDD au Front

Gérer la complexité avec les types opaques

Sébastien BESNIER

Made with Slides.com