Définir proprement des paramètres nommés optionnels
Par Christophe Porteneuve • Publié le 15 mai 2020
• 4 min
This page is also available in English.
Voici le douzième article de notre série quotidienne « 19 pépites de JS pur ». Connais-tu la notion de « paramètres nommés » ou “keyword parameters” ? C’est très pratique mais ça n’existe pas à proprement parler en JavaScript. Heureusement, on a depuis longtemps des solutions, qui sont devenues encore plus pratiques à mettre en œuvre avec ES2015…
Dans la série…
Extrait de la liste des articles :
- Trier proprement des textes
- Extraire les emojis d’un texte
- Définir proprement des paramètres nommés optionnels (cet article)
const
is the newvar
- Utiliser des captures nommées
- …au-delà, c’est la surprise ! (mais la liste est déjà calée)…
Pas de « vrais » paramètres nommés…
De nombreux langages ont une fonctionnalité communément appelée « paramètres nommés » (“keyword parameters” en anglais), qui offre un confort indiscutable, car on peut alors :
- nommer les paramètres pour lesquels on passe un argument, ce qui clarifie l’appel ;
- passer uniquement les arguments dont on a besoin ;
- ne plus dépendre de l’ordre déclaratif lors de l’appel.
Exemple en Ruby :
def smart_slice(from:, to: -1)
# Ici on a deux paramètres nommés :
# - `from`, qui est manifestement obligatoire
# - `to`, qui par défaut vaudrait -1
end
# Juste l’obligatoire
smart_slice(from: -5)
# Les deux (l’ordre importe peu)
smart_slice(from: -3, to: -2)
smart_slice(to: -2, from: -3)
On trouve ça en Kotlin, Python, Swift… En C#, n’importe quel argument peut être nommé à l’appel (à condition qu’il n’y ait pas d’argument anonyme ensuite). Bref, c’est courant.
En JavaScript, on n’a pas formellement ça. Dans un appel de fonction, on passe simplement les arguments dans l’ordre des paramètres : l’association est implicite et positionnelle.
Sans forcément tomber dans les signatures de l’enfer, ça peut très facilement donner des trucs pas lisibles :
// Même si on sait qu’il y a deux booléens à définir, l’ordre est…?
setup(true, false)
// 15,15 est sans doute le centre, mais ensuite ? Rayon ? Épaisseur ?
new Circle(15, 15, 8, 3)
Une règle d’or dit que si on a plusieurs arguments consécutifs de même type sans ordre naturel, ou qu’on dépasse 3 arguments (fussent-ils intuitifs), on nomme les paramètres. OK, mais alors en JS comment faire ?!
Le hash d’options, une solution ancienne
Avant que Ruby n’ait formellement les paramètres nommés, il trichait avec un argument Hash
, la syntaxe nous permettant d’omettre les accolades du Hash
à l’appel. En JavaScript aussi on passera par un hash d’options (un objet, quoi), sauf que les accolades restent nécessaires.
Ça fait très longtemps qu’on fait comme ça. Après tout, jQuery.ajax(…)
a 35 options, tu imagines le boxon si on devait les renseigner positionnellement ?!
Bon, quand on faisait ça à l’ancienne, c’était un peu pénible :
// OLD SKOOL
function run(options) {
// Et que je te masse tout ça…
options.timeout = options.timeout == null ? 10 : options.timeout
options.onSuccess = options.onSuccess || noop
options.onError = options.onError || noop
// Code opérationnel
}
run({ timeout: 5, onError: console.error })
Outre la verbosité, la signature ne nous renseignait en rien ; si on n’avait pas de la doc détaillée (ou une définition de types soignée), on était foutus, il fallait regarder le code. Qui plus est, le code commençait justement par du « bruit » plutôt que par l’opérationnel.
Déstructuration nominative de l’argument
L’arrivée de la déstructuration nominative en ES2015 a certes permis de clarifier un peu les choses :
// LESS OLD SKOOL
function run({ timeout, onSuccess, onError }) {
timeout = timeout == null ? 10 : timeout
onSuccess = onSuccess || noop
onError = onError || noop
// Code opérationnel
}
Au moins, la signature est plus descriptive (et on va la retrouver dans la complétion automatique, etc.). Même sans définition de types soignée, la plupart des EDI et éditeurs avancés vont nous proposer la complétion sur les options.
Valeurs par défaut
ES2015 apporte aussi les valeurs par défaut. Celles-ci ne sont utilisées que si la donnée d’origine est à undefined
, ce qui est souvent ce qu’on veut, mais pas toujours. Si par exemple tu considères null
, 0
ou false
comme invalides, il faudra retravailler manuellement tes arguments. Mais bon, ici :
// NEW SCHOOL
function run({ timeout = 10, onSuccess = noop, onError = noop }) {
// Code opérationnel
}
Là aussi, ces infos remonteront en complétion auto lors de l’appel, ce qui est toujours utile.
Et si je ne veux rien passer ?
Le piège classique survient quand ta fonction accepte, en pratique, un appel sans arguments. Par exemple, toutes les options ont une valeur par défaut (ou pour celles qui n’en ont pas, undefined
est considérée acceptable). Tu aimerais alors pouvoir faire un appel sans argument :
run()
// => TypeError: Cannot read property 'timeout' of undefined
Le souci c’est que ta signature déstructure son argument, alors qu’il n’est pas possible de déstructurer null
ou undefined
, d’où la TypeError
.
Comprends bien que si un de tes arguments est obligatoire (par exemple, timeout
), d’une part tu n’y mettras pas de valeur par défaut, d’autre part il est acceptable de faire planter l’appel vide comme ci-dessus.
Mais s’il est souhaitable d’autoriser un appel vide ? Il suffit de fournir une valeur par défaut à l’argument lui-même :
function run({ timeout = 10, onSuccess = noop, onError = noop } = {}) {
// Code opérationnel
}
Ici, faute d’un objet passé, on utilisera l’objet vide. Il est alors déstructuré, et vu qu’il n’a aucune des propriétés correspondant à tes options, les valeurs par défaut individuelles sont exploitées.
Pourquoi mettre les valeurs par défaut dans la déstructuration ?
On me demande parfois pourquoi ne pas plutôt mettre les valeurs par défaut dans l’objet par défaut de premier niveau, comme ceci :
// NE FAIS PAS ÇA !
function run(
{ timeout, onSuccess, onError } = {
timeout: 10,
onSuccess: noop,
onError: noop,
}
) {
// Code opérationnel
}
Outre la répétition des noms d’options, le problème ici c’est que le jour où tu passeras des options, leur objet remplacera ton objet par défaut. Seule la déstructuration s’exécute, et tu n’auras plus de valeurs par défaut pour les options non fournies.
run({ onError: console.error })
// timeout et onSuccess seront à `undefined`, du coup
Donc mets toujours tes valeurs d’option par défaut dans la déstructuration. En prime, c’est plus court à écrire.