Author: saqibkhan

  • Decorators

    Decorators in TypeScript are special kinds of declarations that can be attached to the class declaration, property, accessor, method, and parameters. It is used for the separate modification of the class without modifying the original source code. This makes them a powerful tool in the domain of object-oriented programming, allowing you to write cleaner, more organized code that adheres to the DRY (Don’t Repeat Yourself) principle.

    Using Decorators in TypeScript

    You are required to enable the ‘experimentalDecorators’ compiler option to enable the experimental support for decorators in your TypeScript project.

    There are 2 ways to enable ‘experimentalDecorators’ compiler option in TypeScript. You can use any option.

    • Execute the below command in the terminal in the project directory. tsc –target ES5 –experimentalDecorators
    • Or, you can update the tsconfig.js file and add “experimentalDecorators”: true attribute in compilerOptions object.{“compilerOptions”:{“target”:”ES5″,”experimentalDecorators”:true}}

    Decorator Syntax

    You can follow the syntax below to use decorators in TypeScript.

    @DecoratorName

    In the above syntax, ‘DecoratorName’ is a function name that is prefixed with the ‘@’ symbol. The expression must evaluate the ‘DecorateName’ function at the run time.

    Decorator Factories

    Whenever you need to customize how the decorator function is applied to declarations, they can use the decorator factories. A decorator factory function returns the expression which will be evaluated at the run time.

    Follow the syntax below to use the decorator factory function.

    functiondecoratorName(args:string){// Decorator factory returns the function expressionreturnfunction(target){// This is the decorator which will be evaluated at the run time.};}

    In the above syntax, ‘args’ is an argument passed to the decorator function, and ‘target’ is a prototype of the class.

    Decorator Composition

    You can use multiple decorators with particular declarations. Multiple decorators either can be used in a single line or multiline as shown below.

    In a single line:

    @f@g x
    

    OR, In multi lines:

    @f@g
    x
    

    In the above syntax, ‘f’ and ‘g’ decorators are used with a single declaration ‘x’.

    Why Use Decorators?

    Let’s take a simple example to understand the use cases of decorators.

    // Defining a classclassStudent{// Declaring the properties of the classconstructor(private name:string,private rollNo:number){}// Defining the methods of the classsayHello(){console.log(Hello, my name is ${this.name}.);}printrollNo(){console.log(My RollNo is ${this.rollNo}.);}}// Creating an object of the classconst user =newStudent("John",20);// Accessing the properties of the classuser.sayHello();
    user.printrollNo();

    On compiling, it will generate the following JavaScript code:

    // Defining a classclassStudent{// Declaring the properties of the classconstructor(name, rollNo){this.name = name;this.rollNo = rollNo;}// Defining the methods of the classsayHello(){console.log(Hello, my name is ${this.name}.);}printrollNo(){console.log(My RollNo is ${this.rollNo}.);}}// Creating an object of the classconst user =newStudent("John",20);// Accessing the properties of the classuser.sayHello();
    user.printrollNo();

    It produces the following output:

    Hello, my name is John.
    My RollNo is 20.
    

    Now, what if we want to log the function when the execution of the function starts and ends? We need to add logs at the start and end of each function as shown in the below code.

    // Defining a classclassStudent{// Declaring the properties of the classconstructor(private name:string,private rollNo:number){}// Defining the methods of the classsayHello(){console.log("Start: sayHello");console.log(Hello, my name is ${this.name}.);console.log("End: sayHello");}printrollNo(){console.log("Start: printrollNo");console.log(My RollNo is ${this.rollNo}.);console.log("End: printrollNo");}}// Creating an object of the classconst user =newStudent("John",20);// Accessing the properties of the classuser.sayHello();
    user.printrollNo();

    On compiling, it will generate the following JavaScript code:

    // Defining a classclassStudent{// Declaring the properties of the classconstructor(name, rollNo){this.name = name;this.rollNo = rollNo;}// Defining the methods of the classsayHello(){console.log("Start: sayHello");console.log(Hello, my name is ${this.name}.);console.log("End: sayHello");}printrollNo(){console.log("Start: printrollNo");console.log(My RollNo is ${this.rollNo}.);console.log("End: printrollNo");}}// Creating an object of the classconst user =newStudent("John",20);// Accessing the properties of the classuser.sayHello();
    user.printrollNo();

    It will produce the following output:

    Start: sayHello
    Hello, my name is John.
    End: sayHello
    Start: printrollNo
    My RollNo is 20.
    End: printrollNo
    

    What if we want to reuse the logic of logging the function execution without writing the repeated code? Here, decorators come into the picture.

    Let’s learn it via the example below.

    // Decorator factoryfunctionprintExecution(method:any, _context:any){// Returning a new functionreturnfunction(value:any,...args:any[]){// Logging the method name at the startconsole.log("start:", method.name);// Calling the original methodconst result =method.call(value,...args);// Logging the method name at the endconsole.log("end:", method.name);return result;}}// Defining a classclass Student {// Declaring the properties of the classconstructor(private name:string,private rollNo:number){}// Defining the methods of the class@printExecutionsayHello(){console.log(Hello, my name is ${this.name}.);}@printExecutionprintrollNo(){console.log(My RollNo is ${this.rollNo}.);}}// Creating an object of the classconst user =newStudent("John",20);// Accessing the properties of the classuser.sayHello();
    user.printrollNo();

    The above code prints the same output as the previous code.

    Start: sayHello
    Hello, my name is John.
    End: sayHello
    Start: printrollNo
    My RollNo is 20.
    End: printrollNo
    

    Class Decorators

    Class decorators are used with the class declaration to observe or modify the class definition.

    Example

    // Decorator factoryfunctionLogClass(target:Function){console.log(${target.name} is instantiated);}// Decorator@LogClassclassMyClass{constructor(){console.log("MyClass instance created");}}// Create an instance of the classconst myClassInstance =newMyClass();

    On compilation, it will generate the following JavaScript code:

    // Class definitionclassMyClass{constructor(){console.log("MyClass instance created");}}// DecoratorLogClass(MyClass);// Create an instance of the classconst myClassInstance =newMyClass();

    Output

    MyClass instance created
    MyClass is instantiated
    

    Method Decorators

    The method decorators are used to replace, modify, or observe the method definition. It is used with the method definition.

    Example

    // Decorator factoryfunctionprintExecution(method:any, _context:any){// Returning a new functionreturnfunction(value:any,...args:any[]){// Logging the method name at the startconsole.log("start:", method.name);// Calling the original methodconst result =method.call(value,...args);return result;}}// Defining a classclass Person {constructor(private name:string){}// Decorator@printExecutionprintName(){console.log(Hello, my name is ${this.name}.);}}// Create an object of the classconst user =newPerson("John");// Accessing the properties of the classuser.printName();

    Output

    start: printName
    Hello, my name is John.
    

    Accessor Decorators

    Accessor decorators are used with the get() and set() accessors to observe, replace, and modify the definition of the accessor.

    Example

    // Decorator factoryfunctionLogAccessor(method:any){console.log("Getter called");}// Define a classclass MyClass {private _name:string="MyClass";// Decorator@LogAccessorgetname(){returnthis._name;}}const instance =newMyClass();console.log(instance.name);

    Output

    Getter called
    MyClass
    

    Property Decorators

    Property decorators are used with the property to modify, replace, and observe it.

    Example

    // Decorator function to log the property namefunctionLogProperty(target:any, propertyKey:string){console.log(Property declared: ${propertyKey});}classPerson{// Decorator is applied to the property@LogProperty
    
    name:string;@LogProperty
    age:number;constructor(name:string, age:number){this.name = name;this.age = age;}}let person =newPerson('Jay',23);</code></pre>

    Output

    Property declared: name
    Property declared: age
    

    Parameter Decorators

    Parameter decorators are used with the method parameters to observe, modify, and replace them.

    Example

    // Decorator with parameterfunctionLogParameter(target:any, methodName:string, parameterIndex:number){console.log(Parameter in ${methodName} at index ${parameterIndex} has been decorated);}// Class decoratorclassMyClass{myMethod(@LogParameter param:string){console.log(Executing myMethod with param: ${param});}}// Create an instance of the classconst instance =newMyClass();
    instance.myMethod("test");

    Output

    Parameter in myMethod at index 0 has been decorated
    Executing myMethod with param: test
    

    We have learned to create custom decorators in TypeScript. However, users can use the pre-defined decorators for various purposes like debugging the code, etc.

  • Ambients

    Ambient declarations are a way of telling the TypeScript compiler that the actual source code exists elsewhere. When you are consuming a bunch of third party js libraries like jquery/angularjs/nodejs you cant rewrite it in TypeScript. Ensuring typesafety and intellisense while using these libraries will be challenging for a TypeScript programmer. Ambient declarations help to seamlessly integrate other js libraries into TypeScript.

    Defining Ambients

    Ambient declarations are by convention kept in a type declaration file with following extension (d.ts)

    Sample.d.ts
    

    The above file will not be transcompiled to JavaScript. It will be used for type safety and intellisense.

    The syntax for declaring ambient variables or modules will be as following −

    Syntax

    declare module Module_Name {
    }
    

    The ambient files should be referenced in the client TypeScript file as shown −

    /// <reference path = " Sample.d.ts" />
    

    Example

    Lets understand this with help of an example. Assume you been given a third party javascript library which contains code similar to this.

    FileName: CalcThirdPartyJsLib.js 
    var TutorialPoint;(function(TutorialPoint){var Calc =(function(){functionCalc(){} 
    
      Calc.prototype.doSum=function(limit){var sum =0;for(var i =0; i &lt;= limit; i++){ 
            Calc.prototype.doSum=function(limit){var sum =0;for(var i =0; i &lt;= limit; i++){ 
                  sum = sum + i;return sum;return Calc; 
                  TutorialPoint.Calc = Calc;})(TutorialPoint ||(TutorialPoint ={}));var test =newTutorialPoint.Calc();}}}}}</code></pre>

    As a typescript programmer you will not have time to rewrite this library to typescript. But still you need to use the doSum() method with type safety. What you could do is ambient declaration file. Let us create an ambient declaration file Calc.d.ts

    FileName: Calc.d.ts 
    declaremodule TutorialPoint {exportclassCalc{doSum(limit:number):number;}}

    Ambient files will not contain the implementations, it is just type declarations. Declarations now need to be included in the typescript file as follows.

    FileName : CalcTest.ts  
    /// <reference path = "Calc.d.ts" /> var obj =newTutorialPoint.Calc(); 
    obj.doSum("Hello");// compiler error console.log(obj.doSum(10));

    The following line of code will show a compiler error. This is because in the declaration file we specified the input parameter will be number.

    obj.doSum("Hello");

    Comment the above line and compile the program using the following syntax −

    tsc CalcTest.ts
    

    On compiling, it will generate following JavaScript code(CalcTest.js).

    //Generated by typescript 1.8.10/// <reference path = "Calc.d.ts" />var obj =newTutorialPoint.Calc();// obj.doSum("Hello");console.log(obj.doSum(10));

    In order to execute the code, let us add an html page with script tags as given below. Add the compiled CalcTest.js file and the third party library file CalcThirdPartyJsLib.js.

    <html><body style ="font-size:30px;"><h1>Ambient Test</h1><h2>Calc Test</h2></body><script src ="CalcThirdPartyJsLib.js"></script><script src ="CalcTest.js"></script></html>

    It will display the following page −

    Ambient Declarations

    On the console, you can see the following output −

    55
    

    Similarly, you can integrate jquery.d.ts or angular.d.ts into a project, based on your requirement.

  • Modules

    A module is designed with the idea to organize code written in TypeScript. Modules are broadly divided into −

    • Internal Modules
    • External Modules

    Internal Module

    Internal modules came in earlier version of Typescript. This was used to logically group classes, interfaces, functions into one unit and can be exported in another module. This logical grouping is named namespace in latest version of TypeScript. So internal modules are obsolete instead we can use namespace. Internal modules are still supported, but its recommended to use namespace over internal modules.

    Internal Module Syntax (Old)

    module TutorialPoint { 
       export function add(x, y) {  
    
      console.log(x+y); 
    } }

    Namespace Syntax (New)

    namespace TutorialPoint { 
       export function add(x, y) { console.log(x + y);} 
    }
    

    JavaScript generated in both cases are same

    var TutorialPoint;(function(TutorialPoint){functionadd(x, y){console.log(x + y);} 
       TutorialPoint.add = add;})(TutorialPoint ||(TutorialPoint ={}));

    External Module

    External modules in TypeScript exists to specify and load dependencies between multiple external js files. If there is only one js file used, then external modules are not relevant. Traditionally dependency management between JavaScript files was done using browser script tags (<script></script>). But thats not extendable, as its very linear while loading modules. That means instead of loading files one after other there is no asynchronous option to load modules. When you are programming js for the server for example NodeJs you dont even have script tags.

    There are two scenarios for loading dependents js files from a single main JavaScript file.

    • Client Side – RequireJs
    • Server Side – NodeJs

    Selecting a Module Loader

    To support loading external JavaScript files, we need a module loader. This will be another js library. For browser the most common library used is RequieJS. This is an implementation of AMD (Asynchronous Module Definition) specification. Instead of loading files one after the other, AMD can load them all separately, even when they are dependent on each other.

    Defining External Module

    When defining external module in TypeScript targeting CommonJS or AMD, each file is considered as a module. So its optional to use internal module with in external module.

    If you are migrating TypeScript from AMD to CommonJs module systems, then there is no additional work needed. The only thing you need to change is just the compiler flag Unlike in JavaScript there is an overhead in migrating from CommonJs to AMD or vice versa.

    The syntax for declaring an external module is using keyword export and import.

    Syntax

    //FileName : SomeInterface.ts 
    export interface SomeInterface { 
       //code declarations 
    }
    

    To use the declared module in another file, an import keyword is used as given below. The file name is only specified no extension used.

    import someInterfaceRef = require(./SomeInterface);
    

    Example

    Lets understand this using an example.

    // IShape.ts exportinterfaceIShape{draw();}// Circle.ts import shape =require("./IShape");exportclassCircleimplementsshape.IShape {publicdraw(){console.log("Cirlce is drawn (external module)");}}// Triangle.ts import shape =require("./IShape");exportclassTriangleimplementsshape.IShape {publicdraw(){console.log("Triangle is drawn (external module)");}}// TestShape.ts import shape =require("./IShape");import circle =require("./Circle");import triangle =require("./Triangle");functiondrawAllShapes(shapeToDraw: shape.IShape){
       shapeToDraw.draw();}drawAllShapes(newcircle.Circle());drawAllShapes(newtriangle.Triangle());

    The command to compile the main TypeScript file for AMD systems is −

    tsc --module amd TestShape.ts
    

    On compiling, it will generate following JavaScript code for AMD.

    File:IShape.js

    //Generated by typescript 1.8.10define(["require","exports"],function(require, exports){});

    File:Circle.js

    //Generated by typescript 1.8.10define(["require","exports"],function(require, exports){var Circle =(function(){functionCircle(){}
    
      Circle.prototype.draw=function(){console.log("Cirlce is drawn (external module)");};return Circle;})();
    exports.Circle = Circle;});

    File:Triangle.js

    //Generated by typescript 1.8.10define(["require","exports"],function(require, exports){var Triangle =(function(){functionTriangle(){}
    
      Triangle.prototype.draw=function(){console.log("Triangle is drawn (external module)");};return Triangle;})();
    exports.Triangle = Triangle;});

    File:TestShape.js

    //Generated by typescript 1.8.10define(["require","exports","./Circle","./Triangle"],function(require, exports, circle, triangle){functiondrawAllShapes(shapeToDraw){
    
      shapeToDraw.draw();}drawAllShapes(newcircle.Circle());drawAllShapes(newtriangle.Triangle());});</code></pre>

    The command to compile the main TypeScript file for Commonjs systems is

    tsc --module commonjs TestShape.ts
    

    On compiling, it will generate following JavaScript code for Commonjs.

    File:Circle.js

    //Generated by typescript 1.8.10var Circle =(function(){functionCircle(){}
       Circle.prototype.draw=function(){console.log("Cirlce is drawn");};return Circle;})();
    
    exports.Circle = Circle;

    File:Triangle.js

    //Generated by typescript 1.8.10var Triangle =(function(){functionTriangle(){}
       Triangle.prototype.draw=function(){console.log("Triangle is drawn (external module)");};return Triangle;})();
    exports.Triangle = Triangle;

    File:TestShape.js

    //Generated by typescript 1.8.10var circle =require("./Circle");var triangle =require("./Triangle");functiondrawAllShapes(shapeToDraw){
       shapeToDraw.draw();}drawAllShapes(newcircle.Circle());drawAllShapes(newtriangle.Triangle());

    Output

    Cirlce is drawn (external module)
    Triangle is drawn (external module)
    
  • Namespaces

    A namespace is a way to logically group related code. This is inbuilt into TypeScript unlike in JavaScript where variables declarations go into a global scope and if multiple JavaScript files are used within same project there will be possibility of overwriting or misconstruing the same variables, which will lead to the global namespace pollution problem in JavaScript.

    Defining a Namespace

    A namespace definition begins with the keyword namespace followed by the namespace name as follows −

    namespace SomeNameSpaceName {exportinterfaceISomeInterfaceName{}exportclassSomeClassName{}}

    The classes or interfaces which should be accessed outside the namespace should be marked with keyword export.

    To access the class or interface in another namespace, the syntax will be namespaceName.className

    SomeNameSpaceName.SomeClassName;
    

    If the first namespace is in separate TypeScript file, then it should be referenced using triple slash reference syntax.

    /// <reference path = "SomeFileName.ts" />
    

    The following program demonstrates use of namespaces −

    FileName :IShape.ts 
    ----------namespace Drawing {exportinterfaceIShape{draw();}}  
    
    FileName :Circle.ts 
    ----------/// <reference path = "IShape.ts" /> namespace Drawing {exportclassCircleimplementsIShape{publicdraw(){console.log("Circle is drawn");}  
    
      
      FileName :Triangle.ts 
      ----------/// &lt;reference path = "IShape.ts" /&gt; namespace Drawing {exportclassTriangleimplementsIShape{publicdraw(){console.log("Triangle is drawn");}} 
         
         FileName : TestShape.ts 
         /// &lt;reference path = "IShape.ts" /&gt;   /// &lt;reference path = "Circle.ts" /&gt; /// &lt;reference path = "Triangle.ts" /&gt;  functiondrawAllShapes(shape:Drawing.IShape){ 
            shape.draw();}drawAllShapes(newDrawing.Circle());drawAllShapes(newDrawing.Triangle());}}}</code></pre>

    The above code can be compiled and executed using the following command −

    tsc --out app.js TestShape.ts  
    
    node app.js
    

    On compiling, it will generate following JavaScript code(app.js).

    //Generated by typescript 1.8.10/// <reference path = "IShape.ts" />var Drawing;(function(Drawing){var Circle =(function(){functionCircle(){}
    
      Circle.prototype.draw=function(){console.log("Cirlce is drawn");};return Circle;}());
    Drawing.Circle = Circle;})(Drawing ||(Drawing ={}));/// <reference path = "IShape.ts" />var Drawing;(function(Drawing){var Triangle =(function(){functionTriangle(){}
      Triangle.prototype.draw=function(){console.log("Triangle is drawn");};return Triangle;}());
    Drawing.Triangle = Triangle;})(Drawing ||(Drawing ={}));/// <reference path = "IShape.ts" />/// <reference path = "Circle.ts" />/// <reference path = "Triangle.ts" />functiondrawAllShapes(shape){ shape.draw();}drawAllShapes(newDrawing.Circle());drawAllShapes(newDrawing.Triangle());

    When the above code is compiled and executed, it produces the following result −

    Circle is drawn 
    Triangle is drawn
    

    Nested Namespaces

    You can define one namespace inside another namespace as follows −

    namespace namespace_name1 {exportnamespace namespace_name2 {exportclassclass_name{}}}

    You can access members of nested namespace by using the dot (.) operator as follows −

    FileName : Invoice.ts  
    namespace tutorialPoint {exportnamespace invoiceApp {exportclassInvoice{publiccalculateDiscount(price:number){return price *.40;}}}} 
    FileName: InvoiceTest.ts 
    
    /// <reference path = "Invoice.ts" />var invoice =newtutorialPoint.invoiceApp.Invoice();console.log(invoice.calculateDiscount(500));

    The above code can be compiled and executed using the following command −

    tsc --out app.js InvoiceTest.ts 
    node app.js
    

    On compiling, it will generate following JavaScript code(app.js).

    //Generated by typescript 1.8.10var tutorialPoint;(function(tutorialPoint){var invoiceApp;(function(invoiceApp){var Invoice =(function(){functionInvoice(){}
    
         Invoice.prototype.calculateDiscount=function(price){return price *.40;};return Invoice;}());
      invoiceApp.Invoice = Invoice;})(invoiceApp = tutorialPoint.invoiceApp ||(tutorialPoint.invoiceApp ={}));})(tutorialPoint ||(tutorialPoint ={}));/// &lt;reference path = "Invoice.ts" /&gt;var invoice =newtutorialPoint.invoiceApp.Invoice();console.log(invoice.calculateDiscount(500));</code></pre>

    When the above code is compiled and executed, it produces the following result −

    200
    
  • Triple-Slash Directives

    Triple-slash directives are similar to single-line comments which contain a single XML tag. They are used to provide instructions to the TypeScript compiler about processing the TypeScript file. Triple-slash directives start with three forward slashes (///) and are mainly placed at the top of the code file.

    Triple-slash directives are mostly used in TypeScript. However, they can also be used with JavaScript projects that use TypeScript.

    In TypeScript, you can use triple-slash directives for two purposes.

    Reference Directives

    Reference directives are mainly used to tell the compiler to include another TypeScript file in the compilation process. They specify the dependencies between multiple files. Furthermore, they are also used to declare dependency packages and include libraries in the TypeScript file.

    Syntax

    /// <reference path="file_path" />

    In the above syntax, we have used three forward slashes first to define the triple-slash directives. After that, we used the XML tag <reference /> and added a path attribute to it. The path attribute takes the file path as a value.

    Similarly, you can also use other XML tags with three forward slashes.

    Types of Triple-Slash Reference Directives

    Here are the most commonly used triple-slash reference directives in a JavaScript/TypeScript environment:

    • ///<reference path=”…” /> − It is used to add a reference of one TypeScript file in another file.
    • ///<reference types=”…” /> − It is used to declare a dependency on a package.
    • ///<reference lib=”…” /> − It is used to include a library file that is part of the compilation context.

    Example: Referencing File Paths

    The main purpose of the triple-slash directives is to reference other files in particular TypeScript files.

    Filename: MathFunctions.ts

    In the below code, we have defined the add() function that takes two numbers as a parameter and returns the sum of them. We also export that function using the export keyword to use it in a different file.

    // This file will be referenced in app.tsexportfunctionadd(a:number, b:number):number{return a + b;}

    Filename: app.ts

    In this file, we have imported the add() function from the mathFunctions file. We have also used the reference directive in this file to tell the file path to the compiler.

    /// <reference path="mathFunctions.ts" />import{ add }from'./mathFunctions';console.log('Addition of 10 and 20 is:',add(10,20));

    Output

    Addition of 10 and 20 is 30
    

    Example: Referencing Type Definition

    The triple-slash directives can also be used to specify the reference type definition for the external module. You can use the <reference type=”module_type” /> directive to specify the type of the module.

    App.ts

    In the code below, we have imported the fs module. Before that, we have used the triple-slash directive to specify the type of the module.

    Next, we used the readFileSync() method of the fs module to read the file content.

    /// <reference types="node" />import*as fs from'fs';const content = fs.readFileSync('sample.txt','utf8');console.log('File content:', content);

    Sample.txt

    The below file contains plain text.

    Hello, this is a sample text file.
    

    Output

    File content: Hello, this is a sample text file.
    

    Example: Including Libraries

    When you want to use a specific library during the TypeScript compilation, you can use the ///<reference lib=”lib_name” /> triple-slash directive to specify it.

    In the code below, we have used the triple-slash directive to specify the es2015.array library.

    /// <reference lib="es2015.array" /> const str =["Hello","1","World","2"];const includesTwo = str.includes("2");console.log(includesTwo);

    Output

    true
    

    Module System Directives

    Module system directives are used to specify how modules are loaded in the TypeScript file. They are used to load modules asynchronously.

    There are two types of module system directives in TypeScript:

    • ///<amd-module name=”…” />: It is used to specify the module name for loading.
    • ///<amd-dependency path=”…” />: It is used to specify the path for the dependencies of the AMD module.

    Example

    In the code below, we have used the AMD-module triple-slash directive which specifies to load the MyModule module asynchronously.

    /// <amd-module name="MyModule" />exportfunctiondisplayMessage(){return"Hello from MyModule";}

    Triple-slash directives allow developers to enhance JavaScript projects by providing advanced compilation options, module loading, and integration with various types and libraries.

  • Generic Classes

    Generic Classes

    TypeScript Generic classes allow you to create a class that can work over a variety of data types rather than a single one. It increases the scalability and reusability of the code. Let’s understand how generic classes work in TypeScript.

    Syntax

    You can follow the syntax below to use the generic classes in TypeScript.

    classclass_name<T, U>{// Class body}let obj1 =newclass_name<data_type_1, data_type_2>();
    • In the above syntax, ‘class’ is a keyword to define a class.
    • ‘class_name’ is a valid identifier, representing the class name.
    • ‘<T, U>’ are type parameters specified in the angular bracket. You can specify as many as you want.
    • While defining the object of the class, you need to pass data types as an argument after the class name in the angular bracket.

    Example

    In the code below, we have defined the generic class named ‘Box’ which takes type parameter T.

    In the class, we have defined the ‘val’ variable of type T, and the constructor function which initializes the value of the ‘val’ variable.

    After that, we defined the getters and setters named get() and set(), respectively to get the value of the ‘val’ variable.

    Next, we have defined the ‘box1’ and ‘box2’ objects of the Box class which take number and string data type as a type parameter argument, respectively.

    // generic classclassBox<T>{// member variable
    
    val:T;// constructor with valueconstructor(value:T){this.val = value;}// Method to get valueget():T{returnthis.val;}// Method to set valueset(value:T):void{this.val = value;}}// create object of Box classlet box1 =newBox&lt;number&gt;(10);console.log(box1.get());// 10let box2 =newBox&lt;string&gt;("Hello");console.log(box2.get());// Hello</code></pre>

    On compiling, it will generate the following JavaScript code:

    // generic classclassBox{// constructor with valueconstructor(value){this.val = value;}// Method to get valueget(){returnthis.val;}// Method to set valueset(value){this.val = value;}}// create object of Box classlet box1 =newBox(10);
    console.log(box1.get());// 10let box2 =newBox("Hello");
    console.log(box2.get());// Hello

    Output

    The output of the above code is as follows

    10
    Hello
    

    Example

    In the TypeScript code below:

    • We have defined the 'Stack' class which takes a type parameter 'T'.
    • In the class, we have defined the private variable 'st' whose type is an array of type T.
    • The constructor function initializes the 'st' array.
    • Push() method takes the element of type 'T' as a parameter and inserts it in the 'st' array.
    • The pop() method removes the last element from the 'st' array and returns it.
    • The peek() method returns the last element from the array.
    • The isEmpty() method returns a boolean value based on whether the array is empty.
    • The size() method returns the size of the 'st' array.
    • Next, we have defined the object of the Stack class with the number data type, performed various operations using the methods of the Stack class.
    // Defining the class stackclassStack<T>{// Defining the private array to store the stack elementsprivate st:T[]=[];// Constructor to initialize the stack with initial contentsconstructor(initialContents?:T[]){if(initialContents){this.st = initialContents;}}// Method to push an element to the stackpush(item:T):void{this.st.push(item);}// Method to pop an element from the stackpop():T|undefined{returnthis.st.pop();}// Method to get the top element of the stackpeek():T|undefined{returnthis.st[this.st.length -1];}// Method to check if the stack is emptyisEmpty():boolean{returnthis.st.length ===0;}// Method to get the size of the stacksize():number{returnthis.st.length;}}// Usage Exampleconst numberStack =newStack<number>();
    numberStack.push(1);
    numberStack.push(2);
    numberStack.push(3);console.log(numberStack.peek());// Outputs: 3console.log(numberStack.pop());// Outputs: 3console.log(numberStack.peek());// Outputs: 2console.log(numberStack.isEmpty());// Outputs: falseconsole.log(numberStack.size());// Outputs: 2

    On compiling, it will generate the following JavaScript code:

    // Defining the class stackclassStack{// Constructor to initialize the stack with initial contentsconstructor(initialContents){// Defining the private array to store the stack elementsthis.st =[];if(initialContents){this.st = initialContents;}}// Method to push an element to the stackpush(item){this.st.push(item);}// Method to pop an element from the stackpop(){returnthis.st.pop();}// Method to get the top element of the stackpeek(){returnthis.st[this.st.length -1];}// Method to check if the stack is emptyisEmpty(){returnthis.st.length ===0;}// Method to get the size of the stacksize(){returnthis.st.length;}}// Usage Exampleconst numberStack =newStack();
    numberStack.push(1);
    numberStack.push(2);
    numberStack.push(3);
    console.log(numberStack.peek());// Outputs: 3
    console.log(numberStack.pop());// Outputs: 3
    console.log(numberStack.peek());// Outputs: 2
    console.log(numberStack.isEmpty());// Outputs: false
    console.log(numberStack.size());// Outputs: 2

    Output

    The output of the above code is as follows

    3
    3
    2
    false
    2
    

    Implementing Generic Interface with Generic Classes

    Generic classes can also implement the generic interfaces. So, developers can use a single generic interface to implement multiple generic classes, allowing them to reuse the code.

    Syntax

    You can follow the syntax below to implement the generic interface with generic classes.

    classclass_name<T>implementsinterface_name<T>{// Class body}
    • In the above syntax, 'class class_name<T>' defines the generic class.
    • 'implements' is a keyword to implement the interface with the class.
    • 'interface_name<T>' is a generic interface.

    Example

    In the example below:

    • We have defined the generic interface named 'dataBase', which defines the findById() and save() method.
    • Next, we have defined the generic class named 'memorydataBase', and implemented it with the 'dataBase' interface.
    • In the class, we have defined the 'items' map which stores the numeric value as a key, the value of type 'T'.
    • Next, we have implemented the findById() method, which accesses the value by key from the map and returns it.
    • The save() method stores the key-value pair in the 'items' map.
    • In the end, we created the object of the 'MemorydataBase' class and performed various operations using this method.
    // Defining a generic interfaceinterfacedataBase<T>{findById(id:number):T|undefined;save(item:T):void;}// Defining a class that implements the generic interfaceclassMemorydataBase<T>implementsdataBase<T>{// Defining a private property that is a map of itemsprivate items =newMap<number, T>();// Implementing the findById methodfindById(id:number):T|undefined{returnthis.items.get(id);}// Implementing the save methodsave(item:T):void{const id =this.items.size +1;this.items.set(id, item);}}// Creating an instance of the MemorydataBase classconst repo =newMemorydataBase<string>();
    repo.save("Hello");console.log(repo.findById(1));// Outputs: Hello

    On compiling, it will generate the following JavaScript code:

    // Defining a class that implements the generic interfaceclassMemorydataBase{constructor(){// Defining a private property that is a map of itemsthis.items =newMap();}// Implementing the findById methodfindById(id){returnthis.items.get(id);}// Implementing the save methodsave(item){const id =this.items.size +1;this.items.set(id, item);}}// Creating an instance of the MemorydataBase classconst repo =newMemorydataBase();
    repo.save("Hello");
    console.log(repo.findById(1));// Outputs: Hello

    Output

    The output of the above code is as follows

    Hello

    You may use the 'extends' keyword to use the various constraints with the generic classes. It's always a good idea to use generic parameters, constraints, interfaces, and classes in your code to make it scalable and reusable.

  • Generic Interfaces

    Generic Interfaces

    In TypeScript, the generic interface is similar to the interface but it is defined with one or more type parameters. These type parameters allow the interface to be reused with different data types while still enforcing type safety and consistency.

    An interface in TypeScript is a way to define the structure of the object or class. It specifies the expected format which objects or classes should have, allowing to ensure consistency and type safety in the code.

    Syntax

    You can follow the syntax below to define the generic interfaces.

    interfaceIGeneric<T>{
    
    value1:T;
    value2:T;}</code></pre>

    In the above syntax, 'IGeneric' is a typed interface, which accepts the custom data type. In the interface, we have used the type 'T' as a value of the value1 and value2 properties.

    Example

    In the code below, we have created the 'IGeneric' interface with the custom type 'T'. It has value1 and value2 properties of type 'T'.

    Next, we have defined the 'obj' object of the IGeneric interface type and used the number data type as a generic type with the interface. After that, we print the value of the 'value1' property of the object in the output.

    // Generic interfaceinterfaceIGeneric<T>{
    
    value1:T;
    value2:T;}// Object of generic interfacelet obj: IGeneric&lt;number&gt;={
    value1:10,
    value2:20};console.log(obj.value1);</code></pre>

    On compiling, it will generate the following JavaScript code:

    // Object of generic interfacelet obj ={
    
    value1:10,
    value2:20};
    console.log(obj.value1);

    Output

    The output of the above example code is as follows

    10

    Example

    In this code, we use the 'T' and 'U' both data types with the 'IGeneric' interface. The value1 property is of type 'T' and the value2 property is of type 'U' in the interface.

    Next, we have defined the 'obj' object of interface type with number and string custom data types.

    // Generic interfaceinterfaceIGeneric<T, U>{
    
    value1:T;
    value2:U;}// Define object with a generic interfacelet obj: IGeneric&lt;number,string&gt;={
    value1:10,
    value2:"Hello"};console.log(obj.value2);</code></pre>

    On compiling, it will generate the following JavaScript code:

    // Define object with a generic interfacelet obj ={
    
    value1:10,
    value2:"Hello"};
    console.log(obj.value2);

    Output

    The output of the above example code is as follows

    Hello

    Example

    The below example is very similar to the previous one, but the 'IGeneric' interface contains the merge() method which takes the parameters of type 'U' and returns the value of type 'U'.

    While defining the 'obj' object, we used the number and string data types with the interface. It means the merge() method will take two string parameters, and return a string value.

    In the output, the code prints the concatenated string, which we have passed as parameters of the merge() method.

    // Generic interfaceinterfaceIGeneric<T, U>{
    
    value1:T;// method that returns the value of type Umerge:(a:U, b:U)=&gt;U;}// Define object with a generic interfacelet obj: IGeneric&lt;number,string&gt;={
    value1:10,merge:(a, b)=&gt; a + b
    };console.log(obj.merge("Hello","world!"));// Hello world!

    On compiling, it will generate the following JavaScript code:

    // Define object with a generic interfacelet obj ={
    
    value1:10,merge:(a, b)=&gt; a + b
    }; console.log(obj.merge("Hello","world!"));// Hello world!

    Output

    Helloworld!

    Generic Interface as a Function Type

    Developers can also use the generic interface as a function type. This enables you to use the same function interface across diverse types and scenarios, ensuring type safety without sacrificing flexibility.

    Example

    In the code below, 'func_interface' accepts the generic types T and U. It defines the structure for the function, which takes the parameter of type 'T' and returns the value of type 'U'.

    Next, we have defined the function expression which returns the string length and stored it in the 'stringToLength' variable. The type of the function expression is defined using the generic interface with string and number data types.

    Similarly, we have defined another function that converts a number to a string, and its type is func_interface with string and number data type.

    Next, we invoke the functions and print their output in the console.

    // Define a generic interface for a functioninterfacefunc_interface<T, U>{(input:T):U;}// A specific function that matches the func_interface interfaceconst stringToLength: func_interface<string,number>=(input)=>{return input.length;};// Using the functionconst result =stringToLength("Hello, TypeScript!");// returns 18console.log(result);// Another function that matches the func_interface interfaceconst numberToString: func_interface<number,string>=(input)=>{returnNumber: ${input};};// Using the second functionconst output =numberToString(123);// returns "Number: 123"console.log(output);

    On compiling, it will generate the following JavaScript code:

    // A specific function that matches the func_interface interfaceconststringToLength=(input)=>{return input.length;};// Using the functionconst result =stringToLength("Hello, TypeScript!");// returns 18
    console.log(result);// Another function that matches the func_interface interfaceconstnumberToString=(input)=>{returnNumber: ${input};};// Using the second functionconst output =numberToString(123);// returns "Number: 123"
    console.log(output);

    Output

    18
    Number: 123
    

    In short, generic interfaces allow developers to reuse the interfaces with multiple data types. Generic interfaces can be used as an object or function type, allowing to reuse the single function or object with different structures.

  • Generic Constraints

    In TypeScript, generic constraints allow you to specify limitations on the types that can be used with a generic type parameter. This adds an extra layer of type safety by ensuring the generic code only works with compatible data types.

    Problem Examples

    Before going deep into the generic constraints, let’s understand the problem examples where you need to apply generic constraints.

    Example

    In the code below, we have created the merge() generic function, which takes the two objects as parameters, merges them using the spread operator, and returns the merged object.

    After that, we invoke the merge() function by passing two objects as an argument, and it successfully prints the merged object.

    // Generic function to merge two objectsfunctionmerge<T, U>(obj1:T, obj2:U){return{...obj1,...obj2};}// Invoke the functionconst mergedObj =merge({name:'Sam'},{age:30});console.log(mergedObj);// Output: {name: 'Sam', age: 30}  

    On compiling, it will generate the following JavaScript code.

    // Generic function to merge two objectsfunctionmerge(obj1, obj2){return Object.assign(Object.assign({}, obj1), obj2);}// Invoke the functionconst mergedObj =merge({ name:'Sam'},{ age:30});
    console.log(mergedObj);// Output: {name: 'Sam', age: 30}  

    Output

    The output of the above example code is as follows

    {name: 'Sam', age: 30}
    

    The merge() function has generic parameters. So, it can take the value of any data type as an argument including an object. What if you pass the boolean value as a second argument? Let’s look at the example below.

    Example

    The below example code is very similar to the previous one. We have just changed the second argument of the merge() function to a Boolean value while calling it.

    // Generic function to merge two objectsfunctionmerge<T, U>(obj1:T, obj2:U){return{...obj1,...obj2};}// Invoke the functionconst mergedObj =merge({name:'Sam'},true);console.log(mergedObj);// Output: {name: 'Sam'}  

    On compiling, it will generate the following JavaScript code.

    // Generic function to merge two objectsfunctionmerge(obj1, obj2){return Object.assign(Object.assign({}, obj1), obj2);}// Invoke the functionconst mergedObj =merge({ name:'Sam'},true);
    console.log(mergedObj);// Output: {name: 'Sam'}  

    Output

    The output of the above example code is as follows

    {name: 'Sam'}
    

    The above code prints the first object only in the output because the second argument was a Boolean value but not an object. To solve the problem found in the above example code, developers can use generic constraints.

    How Generic Constraints Work in TypeScript?

    Generic constraints allow us to limit the generic parameters to accept values of only particular types. i.e. you can narrow down the type of the generic parameters.

    Syntax

    You can follow the syntax below to use the generic constraints with generic parameters.

    functionmerge<T extends object>(obj1:T){// Code to execute}
    • In the above syntax, ‘T’ is a generic type, ‘extends’ is a keyword, and ‘object’ is a data type.
    • Here, ‘T’ accepts only values having an ‘object’ data type.

    Let’s understand more about generic constraints via the example below. Now, if you try to compile the below code, you will get the compilation error as the generic parameter can accept only object argument but we are passing Boolean value.

    // Generic function to merge two objectsfunctionmerge<T extends object, U extends object>(obj1:T, obj2:U){return{...obj1,...obj2 };}// Invoke the functionconst mergedObj =merge({ name:'Sam'},true);console.log(mergedObj);

    On compiling the above TypeScript code, the compiler shows the following error

    Argument of type 'boolean' is not assignable to parameter of type 'object'.
    

    This way, we can limit the generic parameters to accept the values of a particular data type.

    Example (Extending Generic Types with Interfaces)

    Let’s understand the code below with a step-by-step explanation.

    • We have defined the ‘Person’ interface which contains name, age, and email properties.
    • Next, we have defined the ‘Employee’ interface which contains ’empCode’, and ’empDept’ properties.
    • The merge() function contains two generic parameters T of type Person and U of type Employee.
    • In the merge() function, we merge both objects.
    • After that, we have defined two objects of type Person, and Employee, respectively.
    • Next, we invoke the merge() function by passing objects as an argument, and the code runs without any error.
    // Define Person interfaceinterfacePerson{
    
    name:string;
    age:number;
    email:string;}// Define Employee interfaceinterface Employee {
    empCode:number;
    empDept:string;}// Generic function which takes Objects of the Person and Employee interfaces typesfunctionmerge&lt;T extends Person, U extends Employee&gt;(obj1:T, obj2:U){return{...obj1,...obj2 };}// Create two objectsconst person: Person ={ name:'John', age:30, email:'[email protected]'};const employee: Employee ={ empCode:1001, empDept:'IT'};// Invoke the functionconst mergedObj =merge(person, employee);console.log(mergedObj);</code></pre>

    On compiling, it will generate the following JavaScript code.

    // Generic function which takes Objects of the Person and Employee interfaces typesfunctionmerge(obj1, obj2){return Object.assign(Object.assign({}, obj1), obj2);}// Create two objectsconst person ={ name:'John', age:30, email:'[email protected]'};const employee ={ empCode:1001, empDept:'IT'};// Invoke the functionconst mergedObj =merge(person, employee);
    console.log(mergedObj);

    Output

    The output of the above example code is as follows

    {
      name: 'John',
      age: 30,
      email: '[email protected]',
      empCode: 1001,
      empDept: 'IT'
    }
    

    Using Type Parameters in Generic Constraints

    TypeScript also allows you to define a type parameter, which is constrained by another parameter of the same function.

    Let's understand it via the example below.

    Example

    In the code below, type 'U' extends the keys of the object received in the first parameter. So, it will accept the keys of the obj object as an argument to avoid errors in the function body.

    Next, we invoke the getValue() function by passing the 'obj' object as an argument. It prints the key value in the output.

    // Parameter U ensures that the key is a valid key of the object T.functiongetValue<T extends object, U extends keyof T>(obj:T, key:U){return obj[key];}// Define an objectconst obj ={
    
    name:'Sam',
    age:34};// Get the value of the key 'name'const name1 =getValue(obj,'name');console.log(name1);// Sam</code></pre>

    On compiling, it will generate the following JavaScript code.

    // Parameter U ensures that the key is a valid key of the object T.functiongetValue(obj, key){return obj[key];}// Define an objectconst obj ={
    
    name:'Sam',
    age:34};// Get the value of the key 'name'const name1 =getValue(obj,'name');
    console.log(name1);// Sam

    Output

    The output of the above example code is as follows

    Sam
    

    We understood that generic constraints are useful to accept the values of the specific data types as a parameter, instead of taking values of all data types.

  • Generics

    Generics are a powerful feature in TypeScript that allow you to write reusable code that can work with different types of data. They act like placeholders that can be filled with specific data types when you use the generic code. This improves code flexibility and maintainability.

    Problem Examples

    Before going deep into TypeScript generics, let’s understand the problem examples where you need to apply generics.

    Let’s start with the example below, where you want to log the value of the variable passed as a parameter.

    Example

    In the code below, we have defined the printVar() function which takes the number value as a parameter and logs the value in the console. Next, we invoke the function by passing 10 as an argument.

    functionprintVar(val:number){console.log(val);// Prints the value of val}printVar(10);// Invokes the function with a number

    On compiling, it will generate the following JavaScript code.

    functionprintVar(val){
    
    console.log(val);// Prints the value of val}printVar(10);// Invokes the function with a number</code></pre>

    Output

    Its output is as follows

    10
    

    Now, let's suppose you want to extend the use case of the printVar() function to print the value of other types of variables like string, boolean, etc. One way of doing that is as shown in the example below.

    Example

    In the code below, the printVar() function can accept the arguments of number, string, or boolean type.

    functionprintVar(val:number|string|boolean){console.log(val);// Prints the value of val}printVar(true);// Invokes the function with a boolean value

    On compiling, it will generate the following JavaScript code.

    functionprintVar(val){
    
    console.log(val);// Prints the value of val}printVar(true);// Invokes the function with a boolean value</code></pre>

    Output

    The output is as follows

    true
    

    What if you want to print the array or object value? You need to extend the types of the 'val' parameter, and it makes the code complex to read.

    Another way to use the parameters of 'any' data type is as shown in the example below.

    Example

    In the code below, the type of the 'val' parameter is any. So, it can accept any type of value as an argument.

    functionprintVar(val:any){console.log(val);// Prints the value of val}printVar("Hello world!");// Invokes the function with a boolean value

    On compiling, it will generate the following JavaScript code

    functionprintVar(val){
    
    console.log(val);// Prints the value of val}printVar("Hello world!");// Invokes the function with a boolean value</code></pre>

    Output

    Its output is as follows

    Hello world!
    

    The problem with the above code is that you won't have a reference to the data type inside the function. Whether you pass a string, number, boolean, array, etc. as a function argument, you will get the 'any' type of the variable in the function.

    Here, generic functions come into the picture.

    TypeScript Generics

    In TypeScript, generics is a concept that allows to creation of reusable components like functions, classes, interfaces, etc. It creates a function, classes, etc. which can work with multiple data types instead of the single data type. In short, it allows developers to create programs that can work with multiple data types and are scalable in the long term.

    Syntax

    Users can follow the syntax below to use the generic variables with functions in TypeScript.

    functionprintVar<T>(val:T){// execute the code}printVar(val);
    • Developers can use the type variable in the angular bracket(<>) after the function name.
    • After that, you can use the type variable T as a type of the parameters.
    • Here, developers can use any valid identifier instead of 'T'.
    • After that, you can call the function with the value of any data type, and the function automatically captures the data type of the variable.

    Example

    In the example below, the printVar() function is a generic function, which takes the value of any data type as an argument, and prints it.

    After that, we have invoked the function with array, object, and boolean value. In the output, users can observe that it prints the value of different types of variables without any error.

    functionprintVar<T>(val:T){// T is a generic typeconsole.log("data: ", val);}let arr =[1,2,3];let obj ={ name:"John", age:25};printVar(arr);// Val is arrayprintVar(obj);// Val is ObjectprintVar(true);// Val is boolean

    On compiling, it will generate the following JavaScript code.

    functionprintVar(val){
    
    console.log("data: ", val);}let arr =&#91;1,2,3];let obj ={ name:"John", age:25};printVar(arr);// Val is arrayprintVar(obj);// Val is ObjectprintVar(true);// Val is boolean</code></pre>

    Output

    The output of the above example code is as follows

    data:  [ 1, 2, 3 ]
    data:  { name: 'John', age: 25 }
    data:  true
    

    Example

    In this code, we printVar() function is a generic function, which takes the type of the variable value passed as a parameter. While invoking the function, we have passed the value of different data types, and users can observe the type of each variable in the output.

    functionprintVar<T>(val:T){// T is a generic typeconsole.log("data: ",typeof val);}printVar(2);// Val is numberprintVar("Hello");// Val is stringprintVar(true);// Val is boolean

    On compiling, it will generate the following JavaScript code.

    functionprintVar(val){
    
    console.log("data: ",typeof val);}printVar(2);// Val is numberprintVar("Hello");// Val is stringprintVar(true);// Val is boolean</code></pre>

    Output

    The output of the above example code is as follows

    data:  number
    data:  string
    data:  boolean
    

    Example

    In the code below, the concatenate() function takes two parameters of type T and U, respectively. It uses the spread operator to concatenate the value of the 'first' and 'second' parameters.

    Next, we call the function to concatenate two strings and arrays. In the output, we can observe that the concatenate() function executes without any error and prints the final output in the console.

    functionconcatenate<T, U>(first:T, second:U):T&U{return{...first,...second};}// Example usage with stringsconst resultString =concatenate("Hello, ","world!");console.log(resultString);// Output: Hello, world!// Example usage with arraysconst resultArray =concatenate([1,2,3],[4,5,6]);console.log(resultArray);// Output: [1, 2, 3, 4, 5, 6]

    On compiling, it will generate the following JavaScript code.

    functionconcatenate(first, second){return Object.assign(Object.assign({}, first), second);}// Example usage with stringsconst resultString =concatenate("Hello, ","world!");
    console.log(resultString);// Output: Hello, world!// Example usage with arraysconst resultArray =concatenate([1,2,3],[4,5,6]);
    console.log(resultArray);// Output: [1, 2, 3, 4, 5, 6]

    Output

    The output of the above example code is as follows

    {
      '0': 'w',
      '1': 'o',
      '2': 'r',
      '3': 'l',
      '4': 'd',
      '5': '!',
      '6': ' '
    }
    { '0': 4, '1': 5, '2': 6 }
    

    Benefits of Generics

    Here are some benefits of using generics in TypeScript.

    • Type Safety: Generics enforce type consistency, reducing runtime errors by catching mistakes at compile time.
    • Code Reusability: Developers can define a single generic function, class, or interface that works with different data types. It reduces the code duplication.
    • Improved Readability: By using Generics, developers can write cleaner and easy-to-read code.
    • Enhanced Performance: You can increase the performance of the application by avoiding unnecessary type casting and checks via using generics.
  • Template Literal Types

    Template string literals are used to create dynamic strings with variables in JavaScript. Similarly, in TypeScript you can use template literal types to create the dynamic types, which is introduced in the TypeScript 4.1 version. The syntax of the template literal types in TypeScript is very similar to the JavaScript template string literals.

    Syntax

    You can follow the syntax below to use the template literal types in TypeScript.

    typetype_name=some_text ${variable_name}
    • In the above syntax, ‘type_name’ is the name of the type.
    • You need to use the backticks (“) to define template literal types.
    • The ‘some_text’ can be any string value that will remain constant.
    • To add any dynamic value in the type, you need to add the variable name inside the ‘${}’ structure.

    Examples

    Let’s understand the Template Literal Types in details with the help of some examples in TypeScript.

    Example: Basic Use

    In the code below, we have defined the ‘World’ type which contains the World as a value. The ‘Greeting’ type is a template literal type whose value changes based on the value of the ‘World’ variable.

    Next, we created the ‘greeting’ variable of type ‘Greeting’ which contains a Hello World value. If we try to assign another value to it, the TypeScript compiler throws an error.

    typeWorld="world";// Defining a template literal typetype Greeting =Hello, ${World};const greeting: Greeting ="Hello, world";// Correct// const wrongGreeting: Greeting = "Hello, everybody"; // Error: This type is "Hello, world" specifically.console.log(greeting);

    On compiling, it will generate the following JavaScript code:

    const greeting ="Hello, world";// Correct// const wrongGreeting: Greeting = "Hello, everybody"; // Error: This type is "Hello, world" specifically.console.log(greeting);

    The above example code will produce the following output:

    Hello, world
    

    Example: Combining with Union Types

    In this code, the ‘size’ type contains the union of multiple type values. The ‘ResponseMessage’ type is a template literal type whose value changes based on the value of the ‘Size’ type.

    The selectSize() function takes a string of type ‘Size’ as a parameter, and returns the value of type ‘ResponseMessage’.

    Next, we call the function by passing ‘medium’ as a function parameter. If we try to pass any other string value than the ‘small’, ‘medium’, and ‘large’ as a function parameter, it will throw an error.

    // Creating a type using literal typetypeSize="small"|"medium"|"large";// Custom template literal typetype ResponseMessage =${Size} size selected;// Function to select sizefunctionselectSize(size: Size): ResponseMessage {return${size} size selected;}// Call the functionconst response: ResponseMessage =selectSize("medium");// "medium size selected"console.log(response);

    On compiling, it will generate the following JavaScript code:

    // Function to select sizefunctionselectSize(size){return${size} size selected;}// Call the functionconst response =selectSize("medium");// "medium size selected"console.log(response);

    Its output is as follows:

    medium size selected
    

    Example: Conditional String Patterns

    In this example, we have used the generic types with constraints. Here, ‘T extends Status’ means, the value of T can be only from the status. The statusMessage type is a combination of the type ‘T’ and ‘status’ strings.

    The printStatus() function takes the type T as a parameter, which can be any one value from the ‘Status’ type, and returns the value of type ‘statusMessage’.

    Next, we have called the printStatus() function by passing the loading value which is a part of the ‘Status’ type.

    // Definition of the Status typetypeStatus="loading"|"success"|"error";// T can be any of the values in the Status typetype StatusMessage<TextendsStatus>=${T}Status;// Function to return a message based on the statusfunctionprintStatus<T extends Status>(status:T): StatusMessage<T>{return${status} Statusas StatusMessage<T>;}// Call the function with the "loading" statusconst loadingMessage =printStatus("Loading");// "loadingStatus"console.log(loadingMessage);

    On compiling, it will generate the following JavaScript code:

    // Function to return a message based on the statusfunctionprintStatus(status){return${status} Status;}// Call the function with the "loading" statusconst loadingMessage =printStatus("Loading");// "loadingStatus"console.log(loadingMessage);

    Its output is as follows:

    Loading Status
    

    Example: API Route Generation

    The below example demonstrates the real-time practical use of the template literal types.

    Then, We have defined the Method type which can have any value from the REST API method type. The Entity type defines the entity objects.

    The getRoute() method takes the entity and method of type Entity and Method, respectively as a parameter. It returns the string value of type APIRoute after combining the entity and method values.

    // Defining the Method and Entity typestypeMethod="get"|"post"|"delete";typeEntity="user"|"post";// Defining the ApiRoute typetype ApiRoute =/${Entity}/${Method};// Defining the getRoute functionfunctiongetRoute(entity: Entity, method: Method): ApiRoute {return/${entity}/${method}as ApiRoute;}// Using the getRoute functionconst userRoute =getRoute("user","post");// "/user/post"console.log(userRoute);

    On compiling, it will generate the following JavaScript code:

    // Defining the getRoute functionfunctiongetRoute(entity, method){return/${entity}/${method};}// Using the getRoute functionconst userRoute =getRoute("user","post");// "/user/post"console.log(userRoute);

    Its output is as follows:

    /user/post
    

    In TypeScript, the template literal type is mainly used when you need to create dynamic types.