11 - Symbols, unique symbol et opaque types
Ce que tu vas apprendre
- Ce qu'est un Symbol en JavaScript et comment TypeScript le type
- La différence entre
symbol,unique symboletSymbol.for - Comment utiliser
unique symbolpour des branded types garantis uniques - Les well-known symbols (Symbol.iterator, Symbol.hasInstance)
Prerequisites
Avoir lu les articles sur les branded types et le déclaration merging.
Symbol : une valeur garantie unique
Un Symbol est un primitif JavaScript dont chaque instance est unique. Meme deux symbols créés avec la meme description sont différents :
typescriptconst a = Symbol("id")
const b = Symbol("id")
console.log(a === b) // false — toujours
C'est la seule valeur en JavaScript ou l'egalite est basee sur l'identité, pas sur le contenu. Les symbols sont utiles comme clés de propriétés garanties sans collision.
typescriptconst SECRET_KEY = Symbol("secret")
const obj = {
[SECRET_KEY]: "hidden value",
name: "visible"
}
console.log(obj[SECRET_KEY]) // "hidden value"
console.log(Object.keys(obj)) // ["name"] — le symbol n'apparait pas
Les propriétés dont la clé est un symbol ne sont pas enumerees par Object.keys, for...in, ou JSON.stringify. Elles sont accessibles uniquement si tu as la référencé au symbol.
unique symbol en TypeScript
En TypeScript, Symbol() a le type symbol. Mais si tu declares un symbol comme const, TypeScript lui donne le type unique symbol :
typescriptconst s1 = Symbol("id") // type: typeof s1 (unique symbol)
let s2 = Symbol("id") // type: symbol
// unique symbol est plus precis que symbol
const s3: typeof s1 = s1 // ✅
const s4: typeof s1 = s2 // ❌ symbol n'est pas assignable a typeof s1
Un unique symbol est un type literal pour les symbols. Comme "hello" est un type literal pour string, typeof mySymbol est un type literal pour un symbol spécifique.
Declarer un unique symbol
typescript// Avec const
const MY_SYMBOL = Symbol("my")
// type: typeof MY_SYMBOL (unique symbol)
// Dans une interface
declare const TAG: unique symbol
interface Tagged {
[TAG]: true
}
Le declare const TAG: unique symbol créé un type unique sans valeur runtime. C'est utile pour les branded types.
Branded types avec unique symbol
Dans l'article sur les branded types, on utilisait un string comme brand. Le risque : deux brands différents avec le meme string sont confondus.
typescript// ❌ Risque de collision
type Brand<T, B extends string> = T & { __brand: B }
type UserId = Brand<string, "Id"> // __brand: "Id"
type OrderId = Brand<string, "Id"> // __brand: "Id" — meme brand !
Avec unique symbol, chaque brand est garanti unique :
typescriptdeclare const UserIdBrand: unique symbol
declare const OrderIdBrand: unique symbol
type UserId = string & { readonly [UserIdBrand]: never }
type OrderId = string & { readonly [OrderIdBrand]: never }
const userId = "user_123" as UserId
const orderId = "order_456" as OrderId
function getUser(id: UserId) { /* ... */ }
getUser(userId) // ✅
getUser(orderId) // ❌ garanti par le compilateur
Meme si les deux brands ont la meme structure (un symbol readonly avec le type never), les symbols sont différents, donc les types sont incompatibles. Aucun risque de collision.
Symbol.for : symbols partages
Symbol.for créé un symbol dans un registre global. Deux appels avec la meme clé retournent le meme symbol :
typescriptconst a = Symbol.for("app.userId")
const b = Symbol.for("app.userId")
console.log(a === b) // true
C'est utile pour partager des symbols entre modules ou entre packages. En TypeScript, Symbol.for retourne symbol (pas unique symbol) puisque la valeur est partageable.
Well-known symbols
JavaScript definit des symbols standards qui permettent de personnaliser le comportement des objets :
typescript// Symbol.iterator — rendre un objet iterable
class Range {
constructor(private start: number, private end: number) {}
[Symbol.iterator](): Iterator<number> {
let current = this.start
const end = this.end
return {
next() {
if (current <= end) {
return { value: current++, done: false }
}
return { value: undefined, done: true }
}
}
}
}
for (const n of new Range(1, 5)) {
console.log(n) // 1, 2, 3, 4, 5
}
Autres well-known symbols utiles :
typescript// Symbol.hasInstance — personnaliser instanceof
class Even {
static [Symbol.hasInstance](value: unknown) {
return typeof value === "number" && value % 2 === 0
}
}
console.log(4 instanceof Even) // true
console.log(3 instanceof Even) // false
// Symbol.toPrimitive — personnaliser la conversion
class Money {
constructor(public amount: number, public currency: string) {}
[Symbol.toPrimitive](hint: string) {
if (hint === "number") return this.amount
return `${this.amount} ${this.currency}`
}
}
Symbols comme clés privees (avant #private)
Avant les champs prives (#field), les symbols etaient la facon d'avoir des propriétés "presque privees" :
typescriptconst _balance = Symbol("balance")
class Account {
[_balance]: number = 0
deposit(amount: number) {
this[_balance] += amount
}
getBalance() {
return this[_balance]
}
}
const account = new Account()
account.deposit(100)
// account[_balance] — accessible seulement si tu as la reference a _balance
Depuis TypeScript 4.3 et les champs prives ECMAScript (#), ce pattern est moins courant. Mais les symbols restent utiles quand tu veux une propriété qui n'est pas enumeree mais qui est accessible avec la bonne référencé.
Symbol dans les types
TypeScript utilise symbol comme type pour les clés :
typescripttype SymbolKeyed = {
[key: symbol]: unknown
}
const key = Symbol("test")
const obj: SymbolKeyed = {
[key]: "value"
}
Et keyof inclut les symbols :
typescriptconst a = Symbol("a")
const b = Symbol("b")
interface Foo {
[a]: string
[b]: number
name: string
}
type Keys = keyof Foo // typeof a | typeof b | "name"
Sur paltemps.fr, j'utilise des symbols pour les metadonnees internes des entités (timestamps de cache, flags de validation) qui ne doivent pas apparaître dans les réponses JSON de l'API.
Résumé
Symbol()créé une valeur unique garantie — utile pour les clés de propriétés sans collisionunique symbolest le type literal d'un symbol spécifique, obtenu avecconst- Les branded types avec
unique symbolsont garantis uniques par le compilateur (pas de collision de strings) - Les well-known symbols (
Symbol.iterator,Symbol.hasInstance) personnalisent le comportement des objets - Les propriétés a clés symbol ne sont pas enumerees par
Object.keysouJSON.stringify
Article précédent : 10 - Déclaration merging
Article suivant : 12 - Pattern matching type avec ts-pattern