Il est facile de développer et de déployer un service Java simple grâce à la magie omniprésente de Spring Boot. Mais comme les classes fermées doivent être testées et les données transformées, les constructeurs, les convertisseurs, les constructeurs d'énumération et les sérialiseurs sont abondants dans votre code et ouvrent la voie à l'enfer du code Java stéréotypé. C'est pourquoi le développement de nouvelles fonctionnalités est souvent retardé. Et, oui, la génération de code fonctionne, mais ce n'est pas très flexible.
TypeScript n'est pas encore bien établi parmi les développeurs backend. Probablement parce qu'il est connu comme un ensemble de fichiers déclaratifs pour ajouter un peu de frappe à JavaScript. Mais encore, il y a une tonne de logique qui prendrait des dizaines de lignes de Java à représenter, et qui peut être représentée en seulement quelques lignes de TypeScript.
De nombreuses fonctionnalités appelées fonctionnalités de TypeScript font en réalité référence à JavaScript. Mais TypeScript peut également être considéré comme un langage à part entière, avec quelques similitudes syntaxiques et conceptuelles avec JavaScript. Alors sortons de JavaScript un instant et jetons un coup d'œil à TypeScript seul: c'est un beau langage avec un système de type extrêmement puissant mais flexible, des tonnes de sucre syntaxique et enfin une sécurité nulle!
Nous avons hébergé un référentiel sur Github avec une application Web Node / TypeScript personnalisée, ainsi que des explications supplémentaires. Il existe également une branche avancée avec un exemple d' architecture d'oignon et plus de concepts de typage non triviaux.
Présentation de TypeScript
Commençons par les bases: TypeScript est un langage de programmation fonctionnel asynchrone qui prend néanmoins en charge les classes et les interfaces, ainsi que les attributs publics, privés et protégés. Par conséquent, le programmeur, lorsqu'il travaille avec ce langage, gagne une flexibilité considérable en travaillant au niveau de la microarchitecture et du style de code. Le compilateur TypeScript peut être configuré de manière dynamique, c'est-à-dire contrôler quels types d'importations sont autorisés, si les fonctions nécessitent des types de retour explicites et si aucune vérification n'est activée au moment de la compilation.
Étant donné que TypeScript compile en JavaScript standard, Node.js est utilisé comme moteur d'exécution principal. En l'absence d'un cadre complet qui ressemble à Spring, un service Web typique utiliserait un cadre plus flexible servant de serveur Web ( Express.js en est un excellent exemple ). Par conséquent, il s'avérera moins «magique», et sa configuration de base et sa configuration seront organisées plus explicitement. Dans ce cas, des services relativement complexes nécessiteront également plus de bricolage dans la configuration. D'un autre côté, la mise en place d'applications relativement petites n'est pas difficile, et, de plus, c'est faisable presque sans étudier au préalable le cadre.
La gestion des dépendances est facile avec le gestionnaire de packages flexible mais puissant de Node, npm.
Les bases
Lors de la définition des classes
public
, les modificateurs de contrôle d'accès sont pris en charge protected
et private
bien connus de la plupart des développeurs:
class Order {
private status: OrderStatus;
constructor(public readonly id: string, isSpecialOrder: boolean) {
[...]
}
}
La classe a maintenant
Order
deux attributs: un status
champ privé et un champ public en id
lecture seule. Dans TypeScript, les arguments du constructeur avec des mots public
- clés protected
ou private
deviennent automatiquement des attributs de classe.
interface User {
id?: string;
name: string;
t_registered: Date;
}
const user: User = { name: 'Bob', t_registered: new Date() };
Notez que puisque TypeScript utilise l'inférence de type, l'objet User peut être instancié même si la classe
User
elle-même n'est pas fournie . Cette approche de type structure est souvent choisie lorsque vous travaillez avec des entités de données pures et ne nécessite aucune méthode ni état interne.
Les génériques sont exprimés en TypeScript de la même manière qu'en Java:
class Repository<T extends StoredEntity> {
findOneById(id: string): T {
[...]
}
}
Système de type puissant
Au cœur du puissant système de type de TypeScript se trouve l'inférence de type; il prend également en charge le typage statique. Cependant, les annotations de type statique sont facultatives si le type de retour ou le type de paramètre peut être déduit du contexte.
TypeScript permet également l'utilisation de types d'union, de types partiels et d'intersections de types, ce qui donne au langage une flexibilité considérable tout en évitant une complexité inutile. Dans TypeScript, vous pouvez également utiliser une valeur spécifique comme type, ce qui est incroyablement utile dans diverses situations.
Énumérations, inférence de type et types d'union
Considérez une situation courante dans laquelle l'état de la commande doit avoir une représentation de type sécurisé (sous forme d'énumération), mais une représentation sous forme de chaîne est également requise pour la sérialisation JSON. Java déclarerait une énumération pour cela, avec un constructeur et un getter pour les valeurs de chaîne.
Dans le premier exemple, les énumérations TypeScript vous permettent d'ajouter directement une représentation sous forme de chaîne. Cela nous laisse avec une représentation d'énumération de type sécurisé qui sérialise automatiquement sa représentation sous forme de chaîne associée.
enum Status {
ORDER_RECEIVED = 'order_received',
PAYMENT_RECEIVED = 'payment_received',
DELIVERED = 'delivered',
}
interface Order {
status: Status;
}
const order: Order = { status: Status.ORDER_RECEIVED };
Notez la dernière ligne de code, où l'inférence de type nous permet d'instancier un objet qui correspond à l'interface
`Order`
. Puisqu'il n'est pas nécessaire de mettre un état ou une logique interne dans notre ordre, nous pouvons nous passer de classes et sans constructeurs.
Certes, il s'avère qu'en partageant l'inférence des types et des types d'union les uns avec les autres, cette tâche peut être résolue encore plus facilement:
interface Order {
status: 'order_received' | 'payment_received' | 'delivered';
}
const orderA: Order = { status: 'order_received' }; //
const orderB: Order = { status: 'new' }; //
Le compilateur TypeScript n'acceptera que la chaîne qui lui a été fournie comme état de commande valide (notez que cela nécessitera toujours la validation du JSON entrant).
Fondamentalement, ces représentations de type fonctionnent avec n'importe quoi. Un type pourrait bien être une union d'un littéral de chaîne, d'un nombre et de tout autre type ou interface personnalisé. Pour des exemples plus intéressants, consultez le Guide de saisie avancée de TypeScript .
Lambdas et arguments fonctionnels
Étant donné que TypeScript est un langage de programmation fonctionnel, il prend en charge les fonctions anonymes, également appelées lambdas, en son cœur.
const evenNumbers = [ 1, 2, 3, 4, 5, 6 ].filter(i => i % 2 == 0);
L'exemple ci-dessus
.filter()
prend une fonction de type (a: T) => boolean
. Cette fonction est représentée par un lambda anonyme i => i % 2 == 0
. Contrairement à Java, où les paramètres fonctionnels doivent avoir une interface fonctionnelle de type explicite, le type lambda peut également être représenté de manière anonyme:
class OrderService {
constructor(callback: (order: Order) => void) {
[...]
}
}
Programmation asynchrone
Étant donné que TypeScript, avec toutes les mises en garde, est un sur-ensemble de JavaScript, la programmation asynchrone est un concept clé dans ce langage. Oui, vous pouvez utiliser des lambdas et des rappels ici, TypeScript a deux mécanismes essentiels pour vous aider à éviter l'enfer des rappels: des promesses et un joli motif
async/await
. Une promesse est essentiellement une valeur de retour immédiate qui promet de renvoyer une valeur spécifique plus tard.
// ,
function fetchUserProfiles(url: string): Promise<UserProfile[]> {
[...]
}
//
function getActiveProfiles(): Promise<UserProfile[]> {
return fetchUserProfiles(URL)
.then(profiles => profiles.filter(profile => profile.active))
.catch(error => handleError(error));
}
Puisque les instructions
.then()
peuvent être enchaînées dans n'importe quel nombre, dans certains cas, le modèle ci-dessus peut conduire à un code assez déroutant. En déclarant une fonction async
et en l'utilisant await
en attendant que la promesse se résolve, vous pouvez écrire ce même code dans un style beaucoup plus synchrone. Dans ce cas également, une opportunité s'ouvre pour l'utilisation d'opérateurs bien connus try/catch
:
// async/await ( , fetchUserProfiles )
async function getActiveProfiles(): Promise<UserProfile[]> {
const allProfiles = await fetchUserProfiles(URL);
return allProfiles.filter(profile => profile.active);
}
// try/catch
async function getActiveProfilesSafe(): Promise<UserProfile[]> {
try {
const allProfiles = await fetchUserProfiles(URL);
return allProfiles.filter(profile => profile.active);
} catch (error) {
handleError(error);
return [];
}
}
Notez que bien que le code ci-dessus semble synchrone, cela n'est qu'apparent (puisqu'une autre promesse est renvoyée ici).
Opérateur d'extension et opérateur de repos: vous simplifier la vie
Lors de l'utilisation de Java, la manipulation de données, la construction, la fusion et la déstructuration d'objets produisent souvent du code stéréotypé en grande quantité. Les classes doivent être définies, les constructeurs, les getters et les setters doivent être générés et les objets doivent être instanciés. Dans les cas de test, il est souvent nécessaire de recourir activement à la réflexion sur des instances simulées de classes fermées.
Dans TypeScript, tout cela peut être géré sans effort avec son sucre syntaxique sans risque de type doux: opérateurs de propagation et de repos.
Tout d'abord, utilisons l'opérateur d'extension de tableau ... pour décompresser le tableau:
const a = [ 'a', 'b', 'c' ];
const b = [ 'd', 'e' ];
const result = [ ...a, ...b, 'f' ];
console.log(result);
// >> [ 'a', 'b', 'c', 'd', 'e', f' ]
C'est pratique, bien sûr, mais le vrai TypeScript commence lorsque vous réalisez que vous pouvez faire de même avec des objets:
interface UserProfile {
userId: string;
name: string;
email: string;
lastUpdated?: Date;
}
interface UserProfileUpdate {
name?: string;
email?: string;
}
const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const update: UserProfileUpdate = { email: 'bob@example.com' };
const updated: UserProfile = { ...userProfile, ...update, lastUpdated: new Date() };
console.log(updated);
// >> { userId: 'abc', name: 'Bob', email: 'bob@example.com', lastUpdated: 2019-12-19T16:09:45.174Z}
Voyons ce qui se passe ici. Fondamentalement, un objet
updated
est créé à l'aide du constructeur d'accolades. Dans ce constructeur, chaque paramètre crée en fait un nouvel objet, en partant de la gauche.
Ainsi, l'objet étendu est utilisé
userProfile
; la première chose qu'il fait est de se copier. Dans la deuxième étape, l'objet étendu y est update
fusionné et réaffecté au premier objet; ceci, encore une fois, crée un nouvel objet. À la dernière étape, le champ est fusionné et réaffecté lastUpdated
, puis un nouvel objet est créé et, par conséquent, l'objet final.
L'utilisation de l'opérateur de diffusion pour créer des copies d'un objet immuable est un moyen très sûr et rapide de traiter les données. Remarque: L'opérateur d'étalement crée une copie superficielle de l'objet. Les éléments d'une profondeur supérieure à un sont ensuite copiés sous forme de liens.
L'opérateur d'extension a également un équivalent de destructeur appelé object rest :
const userProfile: UserProfile = { userId: 'abc', name: 'Bob', email: 'bob@example.com' };
const { userId, ...details } = userProfile;
console.log(userId);
console.log(details);
// >> 'abc'
// >> { name: 'Bob', email: 'bob@example.com' }
Il est maintenant temps de vous asseoir et d'imaginer tout le code que vous auriez à écrire en Java pour effectuer les opérations décrites ci-dessus.
Conclusion. Un peu sur les avantages et les inconvénients
Performance
Étant donné que TypeScript est intrinsèquement asynchrone et dispose d'un environnement d'exécution rapide, il existe de nombreux scénarios dans lesquels un service Node / TypeScript peut entrer en concurrence avec un service Java. Cette pile est particulièrement adaptée aux opérations d'E / S et fonctionnera bien avec de courtes opérations de blocage occasionnelles, telles que le redimensionnement d'une nouvelle image de profil. Cependant, si le but principal d'un service est de faire des calculs sérieux sur le CPU, Node et TypeScript ne sont probablement pas très bien adaptés pour cela.
Type de numéro
Le type utilisé dans TypeScript laisse également beaucoup à désirer
number
, ce qui ne fait pas la distinction entre les valeurs entières et flottantes. La pratique montre que dans de nombreuses applications, cela ne pose absolument aucun problème. Cependant, il est préférable de ne pas utiliser TypeScript si vous écrivez une application pour un compte bancaire ou un service de paiement.
Écosystème
Compte tenu de la popularité de Node.js, il n'est pas surprenant qu'il existe aujourd'hui des centaines de milliers de packages. Mais comme Node est plus jeune que Java, de nombreux packages n'ont pas survécu à autant de versions et la qualité du code dans certaines bibliothèques est clairement médiocre.
Entre autres, il convient de mentionner quelques bibliothèques de qualité avec lesquelles il est très pratique de travailler: par exemple, pour les serveurs Web , l' injection de dépendances et les annotations de contrôleur . Mais, si le service dépendra sérieusement de nombreux programmes tiers bien pris en charge, il est préférable d'utiliser Python, Java ou Clojure.
Développement de fonctionnalités accéléré
Comme nous l'avons vu ci-dessus, l'un des avantages les plus importants de TypeScript est la facilité avec laquelle il est facile d'exprimer une logique, des concepts et des opérations complexes dans ce langage. Le fait que JSON fasse partie intégrante de ce langage et soit aujourd'hui largement utilisé comme format de sérialisation de données pour le transfert de données et le travail avec des bases de données orientées document, dans de telles situations, il semble naturel de recourir à TypeScript. La configuration d'un serveur Node est très rapide, généralement sans dépendances inutiles; cela économisera des ressources système. C'est pourquoi la combinaison de Node.js avec le système de type fort de TypeScript est si efficace pour créer de nouvelles fonctionnalités en un rien de temps.
Enfin, TypeScript est bien assaisonné avec le sucre syntaxique, donc le développement avec lui est agréable et rapide.