One place for hosting & domains

      classes

      How To Use Classes in TypeScript


      The author selected the COVID-19 Relief Fund to receive a donation as part of the Write for DOnations program.

      Introduction

      Classes are a common abstraction used in object-oriented programming (OOP) languages to describe data structures known as objects. These objects may contain an initial state and implement behaviors bound to that particular object instance. In 2015, ECMAScript 6 introduced a new syntax to JavaScript to create classes that internally uses the prototype features of the language. TypeScript has full support for that syntax and also adds features on top of it, like member visibility, abstract classes, generic classes, arrow function methods, and a few others.

      This tutorial will go through the syntax used to create classes, the different features available, and how classes are treated in TypeScript during the compile-time type-check. It will lead you through examples with different code samples, which you can follow along with in your own TypeScript environment.

      Prerequisites

      To follow this tutorial:

      • An environment in which you can execute TypeScript programs to follow along with the examples. To set this up on your local machine, you will need the following:
      • If you do not wish to create a TypeScript environment on your local machine, you can use the official TypeScript Playground to follow along.
      • You will need sufficient knowledge of JavaScript, especially ES6+ syntax, such as destructuring, rest operators, and imports/exports. If you need more information on these topics, reading our How To Code in JavaScript series is recommended.
      • This tutorial will reference aspects of text editors that support TypeScript and show in-line errors. This is not necessary to use TypeScript but does take more advantage of TypeScript features. To gain the benefit of these, you can use a text editor like Visual Studio Code, which has full support for TypeScript out of the box. You can also try out these benefits in the TypeScript Playground.

      All examples shown in this tutorial were created using TypeScript version 4.3.2.

      Creating Classes in TypeScript

      In this section, you will run through examples of the syntax used to create classes in TypeScript. While you will cover some of the fundamental aspects of creating classes with TypeScript, the syntax is mostly the same used to create classes with JavaScript. Because of this, this tutorial will focus on some of the distinguishing features available in TypeScript.

      You can create a class declaration by using the class keyword, followed by the class name and then a {} pair block, as shown in the following code:

      class Person {
      
      }
      

      This snippet creates a new class named Person. You can then create a new instance of the Person class by using the new keyword followed by the name of your class and then an empty parameter list (which may be omitted), as shown in the following highlighted code:

      class Person {
      
      }
      
      const personInstance = new Person();
      

      You can think of the class itself as a blueprint for creating objects with the given shape, while instances are the objects themselves, created from this blueprint.

      When working with classes, most of the time you will need to create a constructor function. A constructor is a method that runs every time a new instance of the class is created. This can be used to initialize values in the class.

      Introduce a constructor to your Person class:

      class Person {
        constructor() {
          console.log("Constructor called");
        }
      }
      
      const personInstance = new Person();
      

      This constructor will log Constructor called to the console when personInstance is created.

      Constructors are similar to normal functions in the way that they accept parameters. Those parameters are passed to the constructor when you create a new instance of your class. Currently, you are not passing any parameters to the constructor, as shown by the empty parameter list () when creating the instance of your class.

      Next, introduce a new parameter called name of type string:

      class Person {
        constructor(name: string) {
          console.log(`Constructor called with name=${name}`);
        }
      }
      
      const personInstance = new Person("Jane");
      

      In the highlighted code, you added a parameter called name of type string to your class constructor. Then, when creating a new instance of the Person class, you are also setting the value of that parameter, in this case to the string "Jane". Finally, you changed the console.log to print the argument to the screen.

      If you were to run this code, you would receive the following output in the terminal:

      Output

      Constructor called with name=Jane

      The parameter in the constructor is not optional here. This means that when you instantiate the class, you must pass the name parameter to the constructor. If you do not pass the name parameter to the constructor, like in the following example:

      const unknownPerson = new Person;
      

      The TypeScript Compiler will give the error 2554:

      Output

      Expected 1 arguments, but got 0. (2554) filename.ts(4, 15): An argument for 'name' was not provided.

      Now that you have declared a class in TypeScript, you will move on to manipulating those classes by adding properties.

      Adding Class Properties

      One of the most useful aspects of classes is their ability to hold data that is internal to each instance created from the class. This is done using properties.

      TypeScript has a few safety checks that differentiate this process from JavaScript classes, including a requirement to initialize properties to avoid them being undefined. In this section, you will add new properties to your class to illustrate these safety checks.

      With TypeScript, you usually have to declare the property first in the body of the class and give it a type. For example, add a name property to your Person class:

      class Person {
        name: string;
      
        constructor(name: string) {
          this.name = name;
        }
      }
      

      In this example, you declare the property name with type string in addition to setting the property in the constructor.

      Note: In TypeScript, you can also declare the visibility of properties in a class to determine where the data can be accessed. In the name: string declaration, the visibility is not declared, which means that the property uses the default public status that is accessible anywhere. If you wanted to control the visibility explicitly, you would put declare this with the property. This will be covered more in depth later in the tutorial.

      You are also able to give a default value to a property. As an example, add a new property called instantiatedAt that will be set to the time the class instance was instantiated:

      class Person {
        name: string;
        instantiatedAt = new Date();
      
        constructor(name: string) {
          this.name = name;
        }
      }
      

      This uses the Date object to set an initial date for the creation of the instance. This code works because the code for the default value is executed when the class constructor is called, which would be equivalent to setting the value on the constructor, as shown in the following:

      class Person {
        name: string;
        instantiatedAt: Date;
      
        constructor(name: string) {
          this.name = name;
          this.instantiatedAt = new Date();
        }
      }
      

      By declaring the default value in the body of the class, you do not need to set the value in the constructor.

      Note that if you set a type for a property in a class, you must also initialize that property to a value of that type. To illustrate this, declare a class property but do not provide an initializer to it, like in the following code:

      class Person {
        name: string;
        instantiatedAt: Date;
      
        constructor(name: string) {
          this.name = name;
        }
      }
      

      instantiatedAt is assigned a type of Date, so must always be a Date object. But since there is no initialization, the property becomes undefined when the class is instantiated. Because of this, the TypeScript Compiler is going to show the error 2564:

      Output

      Property 'instantiatedAt' has no initializer and is not definitely assigned in the constructor. (2564)

      This is an additional TypeScript safety check to ensure that the correct properties are present upon class instantiation.

      TypeScript also has a shortcut for writing properties that have the same name as the parameters passed to the constructor. This shortcut is called parameter properties.

      In the previous example, you set the name property to the value of the name parameter passed to the class constructor. This may become tiresome to write if you add more fields to your class. For example, add a new field called age of type number to your Person class and also add it to the constructor:

      class Person {
        name: string;
        age: number;
        instantiatedAt = new Date();
      
        constructor(name: string, age: number) {
          this.name = name;
          this.age = age;
        }
      }
      

      While this works, TypeScript can reduce such boilerplate code with parameter properties, or properties set in the parameters for the constructor:

      class Person {
        instantiatedAt = new Date();
      
        constructor(
          public name: string,
          public age: number
        ) {}
      }
      

      In this snippet, you removed the name and age property declarations from the class body and moved them to be inside the parameters list of the constructor. When you do that, you are telling TypeScript that those constructor parameters are also properties of that class. This way you do not need to set the property of the class to the value of the parameter received in the constructor, as you did before.

      Note: Notice the visibility modifier public has been explicitly stated in the code. This modifier must be included when setting parameter properties, and will not automatically default to public visibility.

      If you take a look at the compiled JavaScript emitted by the TypeScript Compiler, this code compiles to the following JavaScript code:

      "use strict";
      class Person {
        constructor(name, age) {
          this.name = name;
          this.age = age;
          this.instantiatedAt = new Date();
        }
      }
      

      This is the same JavaScript code that the original example compiles to.

      Now that you have tried out setting properties on TypeScript classes, you can move on to extending classes into new classes with class inheritance.

      Class Inheritance in TypeScript

      TypeScript offers the full capability of JavaScript’s class inheritance, with two main additions: interfaces and abstract classes. An interface is a structure that describes and enforces the shape of a class or an object, like providing type-checking for more complex pieces of data. You can implement an interface in a class to make sure that it has a specific public shape. Abstract classes are classes that serve as the basis for other classes, but cannot be instantiated themselves. Both of these are implemented via class inheritance.

      In this section, you will run through some examples of how interfaces and abstract classes can be used to build and create type checks for classes.

      Implementing Interfaces

      Interfaces are useful to specify a set of behaviors that all implementations of that interface must possess. Interfaces are created by using the interface keyword followed by the name of the interface, and then the interface body. As an example, create a Logger interface that could be used to log important data about how your program is running:

      interface Logger {}
      

      Next, add four methods to your interface:

      interface Logger {
        debug(message: string, metadata?: Record<string, unknown>): void;
        info(message: string, metadata?: Record<string, unknown>): void;
        warning(message: string, metadata?: Record<string, unknown>): void;
        error(message: string, metadata?: Record<string, unknown>): void;
      }
      

      As shown in this code block, when creating the methods in your interface, you do not add any implementation to them, just their type information. In this case, you have four methods: debug, info, warning, and error. All of them share the same type signature: They receive two parameters, a message of type string and an optional metadata parameter of type Record<string, unknown>. They all return the void type.

      All classes implementing this interface must have the corresponding parameters and return types for each of these methods. Implement the interface in a class named ConsoleLogger, which logs all messages using console methods:

      class ConsoleLogger implements Logger {
        debug(message: string, metadata?: Record<string, unknown>) {
          console.info(`[DEBUG] ${message}`, metadata);
        }
        info(message: string, metadata?: Record<string, unknown>) {
          console.info(message, metadata);
        }
        warning(message: string, metadata?: Record<string, unknown>) {
          console.warn(message, metadata);
        }
        error(message: string, metadata?: Record<string, unknown>) {
          console.error(message, metadata);
        }
      }
      

      Notice that when creating your interface, you are using a new keyword called implements to specify the list of interfaces your class implements. You can implement multiple interfaces by adding them as a comma-separated list of interface identifiers after the implements keyword. For example, if you had another interface called Clearable:

      interface Clearable {
        clear(): void;
      }
      

      You could implement it in the ConsoleLogger class by adding the following highlighted code:

      class ConsoleLogger implements Logger, Clearable {
        clear() {
          console.clear();
        }
        debug(message: string, metadata?: Record<string, unknown>) {
          console.info(`[DEBUG] ${message}`, metadata);
        }
        info(message: string, metadata?: Record<string, unknown>) {
          console.info(message, metadata);
        }
        warning(message: string, metadata?: Record<string, unknown>) {
          console.warn(message, metadata);
        }
        error(message: string, metadata?: Record<string, unknown>) {
          console.error(message, metadata);
        }
      }
      

      Notice that you also have to add the clear method to make sure the class adheres to the new interface.

      If you did not provide the implementation for one of the members required by any of the interfaces, like the debug method from the Logger interface, the TypeScript compiler would give you the error 2420:

      Output

      Class 'ConsoleLogger' incorrectly implements interface 'Logger'. Property 'debug' is missing in type 'ConsoleLogger' but required in type 'Logger'. (2420)

      The TypeScript Compiler would also show an error if your implementation did not match the one expected by the interface you are implementing. For example, if you changed the type of the message parameter in the debug method from string to number, you would receive error 2416:

      Output

      Property 'debug' in type 'ConsoleLogger' is not assignable to the same property in base type 'Logger'. Type '(message: number, metadata?: Record<string, unknown> | undefined) => void' is not assignable to type '(message: string, metadata: Record<string, unknown>) => void'. Types of parameters 'message' and 'message' are incompatible. Type 'string' is not assignable to type 'number'. (2416)

      Building on Abstract Classes

      Abstract classes are similar to normal classes, with two major differences: They cannot be directly instantiated and they may contain abstract members. Abstract members are members that must be implemented in inheriting classes. They do not have an implementation in the abstract class itself. This is useful because you can have some common functionality in the base abstract class, and more specific implementations in the inheriting classes. When you mark a class as abstract, you are saying that this class has missing functionality that should be implemented in inheriting classes.

      To create an abstract class, you add the abstract keyword before the class keyword, like in the highlighted code:

      abstract class AbstractClassName {
      
      }
      

      Next, you can create members in your abstract class, some that may have an implementation and others that will not. Ones without implementation are marked as abstract and must then be implemented in the classes that extend from your abstract class.

      For example, imagine you are working in a Node.js environment and you are creating your own Stream implementation. For that, you are going to have an abstract class called Stream with two abstract methods, read and write:

      declare class Buffer {
        from(array: any[]): Buffer;
        copy(target: Buffer, offset?: number): void;
      }
      
      abstract class Stream {
      
        abstract read(count: number): Buffer;
      
        abstract write(data: Buffer): void;
      }
      

      The Buffer object here is a class available in Node.js that is used to store binary data. The declare class Buffer statement at the top allows the code to compile in a TypeScript environment without the Node.js type declarations, like TypeScript Playground.

      In this example, the read method counts bytes from the internal data structure and returns a Buffer object, and write writes all the contents of the Buffer instance to the stream. Both of these methods are abstract, and can only be implemented in classes extended from Stream.

      You can then create additional methods that do have an implementation. This way any class extending from your Stream abstract class would receive those methods automatically. One such example would be a copy method:

      declare class Buffer {
        from(array: any[]): Buffer;
        copy(target: Buffer, offset?: number): void;
      }
      
      abstract class Stream {
      
        abstract read(count: number): Buffer;
      
        abstract write(data: Buffer): void;
      
        copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
          const data = this.read(count);
          data.copy(targetBuffer, targetBufferOffset);
        }
      }
      

      This copy method copies the result from reading the bytes from the stream to the targetBuffer, starting at targetBufferOffset.

      If you then create an implementation for your Stream abstract class, like a FileStream class, the copy method would be readily available, without having to duplicate it in your FileStream class:

      declare class Buffer {
        from(array: any[]): Buffer;
        copy(target: Buffer, offset?: number): void;
      }
      
      abstract class Stream {
      
        abstract read(count: number): Buffer;
      
        abstract write(data: Buffer): void;
      
        copy(count: number, targetBuffer: Buffer, targetBufferOffset: number) {
          const data = this.read(count);
          data.copy(targetBuffer, targetBufferOffset);
        }
      }
      
      class FileStream extends Stream {
        read(count: number): Buffer {
          // implementation here
          return new Buffer();
        }
      
        write(data: Buffer) {
          // implementation here
        }
      }
      
      const fileStream = new FileStream();
      

      In this example, the fileStream instance automatically has the copy method available on it. The FileStream class also had to implement a read and a write method explicitly to adhere to the Stream abstract class.

      If you had forgotten to implement one of the abstract members of the abstract class you are extending from, like not adding the write implementation in your FileStream class, the TypeScript compiler would give error 2515:

      Output

      Non-abstract class 'FileStream' does not implement inherited abstract member 'write' from class 'Stream'. (2515)

      The TypeScript compiler would also display an error if you implemented any of the members incorrectly, like changing the type of the first parameter of the write method to be of type string instead of Buffer:

      Output

      Property 'write' in type 'FileStream' is not assignable to the same property in base type 'Stream'. Type '(data: string) => void' is not assignable to type '(data: Buffer) => void'. Types of parameters 'data' and 'data' are incompatible. Type 'Buffer' is not assignable to type 'string'. (2416)

      With abstract classes and interfaces, you are able to put together more complex type-checking for your classes to ensure that classes extended from base classes inherit the correct functionality. Next, you will run through examples of how method and property visibility work in TypeScript.

      Class Members Visibility

      TypeScript augments the available JavaScript class syntax by allowing you to specify the visibility of the members of a class. In this case, visibility refers to how code outside of an instantiated class can interact with a member inside the class.

      Class members in TypeScript may have three possible visibility modifiers: public, protected, and private. public members may be accessed outside of the class instance, where as private ones cannot. protected occupies a middle ground between the two, where members can be accessed by instances of the class or subclasses based on that class.

      In this section, you are going to examine the available visibility modifiers and learn what they mean.

      public

      This is the default visibility of class members in TypeScript. When you do not add the visibility modifier to a class member, it is the same as setting it to public. Public class members may be accessed anywhere, without any restrictions.

      To illustrate this, return to your Person class from earlier:

      class Person {
        public instantiatedAt = new Date();
      
        constructor(
          name: string,
          age: number
        ) {}
      }
      

      This tutorial mentioned that the two properties name and age had public visibility by default. To declare type visibility explicitly, add the public keyword before the properties and a new public method to your class called getBirthYear, which retrieves the year of birth for the Person instance:

      class Person {
        constructor(
          public name: string,
          public age: number
        ) {}
      
        public getBirthYear() {
          return new Date().getFullYear() - this.age;
        }
      }
      

      You can then use the properties and methods in the global space, outside the class instance:

      class Person {
        constructor(
          public name: string,
          public age: number
        ) {}
      
        public getBirthYear() {
          return new Date().getFullYear() - this.age;
        }
      }
      
      const jon = new Person("Jon", 35);
      
      console.log(jon.name);
      console.log(jon.age);
      console.log(jon.getBirthYear());
      

      This code would print the following to the console:

      Output

      Jon 35 1986

      Notice that you can access all the members of your class.

      protected

      Class members with the protected visibility are only allowed to be used inside the class they are declared in or in the subclasses of that class.

      Take a look at the following Employee class and the FinanceEmployee class that is based on it:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {
        getFinanceIdentifier() {
          return `fin-${this.identifier}`;
        }
      }
      

      The highlighted code shows the identifier property declared with protected visibility. The this.identifier code tries to access this property from the FinanceEmployee subclass. This code would run without error in TypeScript.

      If you tried to use that method from a place that is not inside the class itself, or inside a subclass, like in the following example:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {
        getFinanceIdentifier() {
          return `fin-${this.identifier}`;
        }
      }
      
      const financeEmployee = new FinanceEmployee('abc-12345');
      financeEmployee.identifier;
      

      The TypeScript compiler would give us the error 2445:

      Output

      Property 'identifier' is protected and only accessible within class 'Employee' and its subclasses. (2445)

      This is because the identifier property of the new financeEmployee instance cannot be retrieved from the global space. Instead, you would have to use the internal method getFinanceIdentifier to return a string that included the identifier property:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {
        getFinanceIdentifier() {
          return `fin-${this.identifier}`;
        }
      }
      
      const financeEmployee = new FinanceEmployee('abc-12345');
      console.log(financeEmployee.getFinanceIdentifier())
      

      This would log the following to the console:

      Output

      fin-abc-12345

      private

      Private members are only accessible inside the class that declares them. This means that not even subclasses have access to it.

      Using the previous example, turn the identifier property in the Employee class into a private property:

      class Employee {
        constructor(
          private identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {
        getFinanceIdentifier() {
          return `fin-${this.identifier}`;
        }
      }
      

      This code will now cause the TypeScript compiler to show the error 2341:

      Output

      Property 'identifier' is private and only accessible within class 'Employee'. (2341)

      This happens because you are accessing the property identifier in the FinanceEmployee subclass, and this is not allowed, as the identifier property was declared in the Employee class and has its visibility set to private.

      Remember that TypeScript is compiled to raw JavaScript that by itself does not have any way to specify the visibility of the members of a class. As such, TypeScript has no protection against such usage during runtime. This is a safety check done by the TypeScript compiler only during compilation.

      Now that you’ve tried out visibility modifiers, you can move on to arrow functions as methods in TypeScript classes.

      Class Methods as Arrow Functions

      In JavaScript, the this value that represents a function’s context can change depending on how a function is called. This variability can sometimes be confusing in complex pieces of code. When working with TypeScript, you can use a special syntax when creating class methods to avoid this being bound to something else other than the class instance. In this section, you will try out this syntax.

      Using your Employee class, introduce a new method used only to retrieve the employee identifier:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      }
      

      This works pretty well if you call the method directly:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      }
      
      const employee = new Employee("abc-123");
      
      console.log(employee.getIdentifier());
      

      This would print the following to the console’s output:

      Output

      abc-123

      However, if you stored the getIdentifier instance method somewhere for it to be called later, like in the following code:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      }
      
      const employee = new Employee("abc-123");
      
      const obj = {
        getId: employee.getIdentifier
      }
      
      console.log(obj.getId());
      

      The value would be inaccessible:

      Output

      undefined

      This happens because when you call obj.getId(), the this inside employee.getIdentifier is now bound to the obj object, and not to the Employee instance.

      You can avoid this by changing your getIdentifier to be an arrow function. Check the highlighted change in the following code:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier = () => {
          return this.identifier;
        }
      }
      ...
      

      If you now try to call obj.getId() like you did before, the console correctly shows:

      Output

      abc-123

      This demonstrates how TypeScript allows you to use arrow functions as direct values of class methods. In the next section, you will learn how to enforce classes with TypeScript’s type-checking.

      Using Classes as Types

      So far this tutorial has covered how to create classes and use them directly. In this section, you will use classes as types when working with TypeScript.

      Classes are both a type and a value in TypeScript, and as such, can be used both ways. To use a class as a type, you use the class name in any place that TypeScript expects a type. For example, given the Employee class you created previously:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      

      Imagine you wanted to create a function that prints the identifier of any employee. You could create such a function like this:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      function printEmployeeIdentifier(employee: Employee) {
        console.log(employee.identifier);
      }
      

      Notice that you are setting the employee parameter to be of type Employee, which is the exact name of your class.

      Classes in TypeScript are compared against other types, including other classes, just like other types are compared in TypeScript: structurally. This means that if you had two different classes that both had the same shape (that is, the same set of members with the same visibility), both can be used interchangeably in places that would expect only one of them.

      To illustrate this, imagine you have another class in your application called Warehouse:

      class Warehouse {
        constructor(
          public identifier: string
        ) {}
      }
      

      It has the same shape as Employee. If you tried to pass an instance of it to printEmployeeIdentifier:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      class Warehouse {
        constructor(
          public identifier: string
        ) {}
      }
      
      function printEmployeeIdentifier(employee: Employee) {
        console.log(employee.identifier);
      }
      
      const warehouse = new Warehouse("abc");
      
      printEmployeeIdentifier(warehouse);
      

      The TypeScript compiler would not complain. You could even use just a normal object instead of the instance of a class. As this may result in a behavior that is not expected by a programmer that is just starting with TypeScript, it is important to keep an eye on these scenarios.

      With the basics of using a class as a type out of the way, you can now learn how to check for specific classes, rather than just the shape.

      The Type of this

      Sometimes you will need to reference the type of the current class inside some methods in the class itself. In this section, you will find out how to use this to accomplish this.

      Imagine you had to add a new method to your Employee class called isSameEmployeeAs, which would be responsible for checking if another employee instance references the same employee as the current one. One way you could do this would be like the following:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      
        isSameEmployeeAs(employee: Employee) {
          return this.identifier === employee.identifier;
        }
      }
      

      This test will work to compare the identifier property of all classes derived from Employee. But imagine a scenario in which you do not want specific subclasses of Employee to be compared at all. In this case, instead of receiving the boolean value of the comparison, you would want TypeScript to report an error when two different subclasses are compared.

      For example, create two new subclasses for employees in the finance and marketing departments:

      ...
      class FinanceEmployee extends Employee {
        specialFieldToFinanceEmployee="";
      }
      
      class MarketingEmployee extends Employee {
        specialFieldToMarketingEmployee="";
      }
      
      const finance = new FinanceEmployee("fin-123");
      const marketing = new MarketingEmployee("mkt-123");
      
      marketing.isSameEmployeeAs(finance);
      

      Here you derive two classes from the Employee base class: FinanceEmployee and MarketingEmployee. Each one has different new fields. You are then creating one instance of each one, and checking if the marketing employee is the same as the finance employee. Given this scenario, TypeScript should report an error, since subclasses should not be compared at all. This does not happen because you used Employee as the type of the employee parameter in your isSameEmployeeAs method, and all classes derived from Employee will pass the type-checking.

      To improve this code, you could use a special type available inside classes, which is the this type. This type is dynamically set to the type of the current class. This way, when this method is called in a derived class, this is set to the type of the derived class.

      Change your code to use this instead:

      class Employee {
        constructor(
          protected identifier: string
        ) {}
      
        getIdentifier() {
          return this.identifier;
        }
      
        isSameEmployeeAs(employee: this) {
          return this.identifier === employee.identifier;
        }
      }
      
      class FinanceEmployee extends Employee {
        specialFieldToFinanceEmployee="";
      }
      
      class MarketingEmployee extends Employee {
        specialFieldToMarketingEmployee="";
      }
      
      const finance = new FinanceEmployee("fin-123");
      const marketing = new MarketingEmployee("mkt-123");
      
      marketing.isSameEmployeeAs(finance);
      

      When compiling this code, the TypeScript compiler will now show the error 2345:

      Output

      Argument of type 'FinanceEmployee' is not assignable to parameter of type 'MarketingEmployee'. Property 'specialFieldToMarketingEmployee' is missing in type 'FinanceEmployee' but required in type 'MarketingEmployee'. (2345)

      With the this keyword, you can change typing dynamically in different class contexts. Next, you will use typing for passing in a class itself, rather than an instance of a class.

      Using Construct Signatures

      There are times when a programmer needs to create a function that takes a class directly, instead of an instance. For that, you need to use a special type with a construct signature. In this section, you will go through how to create such types.

      One particular scenario in which you may need to pass in a class itself is a class factory, or a function that generates new instances of classes that are passed in as arguments. Imagine you want to create a function that takes a class based on Employee, creates a new instance with an incremented identifier, and prints the identifier to the console. One may try to create this like the following:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      let identifier = 0;
      function createEmployee(ctor: Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      

      In this snippet, you create the Employee class, initialize the identifier, and create a function that instantiates a class based on a constructor parameter ctor that has the shape of Employee. But if you tried to compile this code, the TypeScript compiler would give the error 2351:

      Output

      This expression is not constructable. Type 'Employee' has no construct signatures. (2351)

      This happens because when you use the name of your class as the type for ctor, the type is only valid for instances of the class. To get the type of the class constructor itself, you have to use typeof ClassName. Check the following highlighted code with the change:

      class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      let identifier = 0;
      function createEmployee(ctor: typeof Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      

      Now your code will compile successfully. But there is still a pending issue: Since class factories build instances of new classes built from a base class, using abstract classes could improve the workflow. However, this will not work initially.

      To try this out, turn the Employee class into an abstract class:

      abstract class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      let identifier = 0;
      function createEmployee(ctor: typeof Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      

      The TypeScript compiler will now give the error 2511:

      Output

      Cannot create an instance of an abstract class. (2511)

      This error shows that you cannot create an instance from the Employee class, since it is abstract. But you may want to use such a function to create different kinds of employees that extend from your Employee abstract class, like such:

      abstract class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {}
      
      class MarketingEmployee extends Employee {}
      
      let identifier = 0;
      function createEmployee(ctor: typeof Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      
      createEmployee(FinanceEmployee);
      createEmployee(MarketingEmployee);
      

      To make your code work for this scenario, you have to use a type with a constructor signature. You can do this by using the new keyword, followed by a syntax similar to that of an arrow function, where the parameter list contains the parameters expected by the constructor and the return type is the class instance this constructor returns.

      Highlighted in the following code is the change introducing the type with a constructor signature to your createEmployee function:

      abstract class Employee {
        constructor(
          public identifier: string
        ) {}
      }
      
      class FinanceEmployee extends Employee {}
      
      class MarketingEmployee extends Employee {}
      
      let identifier = 0;
      function createEmployee(ctor: new (identifier: string) => Employee) {
        const employee = new ctor(`test-${identifier++}`);
        console.log(employee.identifier);
      }
      
      createEmployee(FinanceEmployee);
      createEmployee(MarketingEmployee);
      

      The TypeScript compiler now will correctly compile your code.

      Conclusion

      Classes in TypeScript are even more powerful than they are in JavaScript because you have access to the type system, extra syntax like arrow function methods, and completely new features like member visibility and abstract classes. This offers a way for you to deliver code that is type-safe, more reliable, and that better represents the business model of your application.

      For more tutorials on TypeScript, check out our How To Code in TypeScript series page.



      Source link

      How To Create Classes With CSS



      Part of the Series:
      How To Build a Website With CSS

      This tutorial is part of a series on creating and customizing this website with CSS, a stylesheet language used to control the presentation of websites. You may follow the entire series to recreate the demonstration website and gain familiarity with CSS or use the methods described here for other CSS website projects.

      Before proceeding, we recommend that you have some knowledge of HTML, the standard markup language used to display documents in a web browser. If you don’t have familiarity with HTML, you can follow the first ten tutorials of our series How To Build a Website With HTML before starting this series.

      Introduction

      In this tutorial, you will create a CSS class selector, which will allow you to apply CSS rules only to HTML elements that are assigned the class. CSS class selectors are useful when you want to apply different style rules for different instances of the same HTML element.

      Prerequisites

      To follow this tutorial, make sure you have set up the necessary files and folders as instructed in a previous tutorial in this series How To Set Up You CSS and HTML Practice Project.

      How CSS Class Selectors Work

      A CSS class selector allows you to assign style rules to HTML elements that you designate with that class rather than all instances of a certain element. Unlike HTML elements (such as <p>, <h1> or <img>), whose names are predetermined, class names are chosen by the developer when they create the class. Class names are always preceded by a ., which can help you distinguish between tag selectors and class selectors in CSS files.

      A CSS rule for a class selector is written in the same way as a rule for a tag selector, with the exception of the . prepended to the class name:

      .red-text {
        color: red;
      }
      

      To use a class when adding HTML content to your webpage, you must specify it in the opening tag of an HTML element using the class attribute in your HTML document like so:

      <h1 class=".red-text">Content.</element>
      

      Creating a CSS Class Using a Class Selector

      Let’s begin exploring CSS classes in practice. Erase everything in your styles.css file and add the following code snippet to specify a rule for the class red-text:

      styles.css

      .red-text {
        color: red;
      }
      

      After adding the code snippet to your styles.css file, save the file.

      Return to your index.html and erase everything but the first line of code <link rel="stylesheet" href="https://www.digitalocean.com/community/tutorials/css/styles.css"> that links to your CSS stylesheet. Then add the following HTML code snippet:

      index.html

      <p class="red-text">Here is the first sample of paragraph text.</p>
      

      Note that the class name is not prepended here with a . as it is when being used as a selector for a CSS rule. Your entire index.html file should have the following contents:

      index.html

      . . .
      <link rel="stylesheet" href="https://www.digitalocean.com/community/tutorials/css/styles.css">
      <p class="red-text" Here is the first sample of paragraph text.</p>
      

      In this code snippet you have added text using the HTML <p> tag. But you have also specified the red-text class by adding the highlighted class attribute class="red-text" inside the opening HTML tag.

      Save your index.html file and load it in the browser. (For instructions on loading an HTML file, please visit our tutorial step How To View An Offline HTML File In Your Browser).

      You should receive a webpage with red text:

      Webpage output with red paragraph text

      Let’s add an additional CSS class to explore styling different pieces of <p> text content with different classes. Add the following code snippet to your styles.css file (after your CSS rule for “red-text”):

      styles.css

      .yellow-background-text {
        background-color: yellow;
      }
      

      This CSS rule declares that the class yellow-background-text is assigned the yellow value for the background-color property. Any HTML text element assigned this class will have a yellow background. Note that the use of the word textin the class yellow-background-*text* is for human readability purposes only. You do not need to include the word text in your class names for classes assigned to HTML text.

      To apply this new CSS class, return to your index.html file and add the following line of code to the bottom:

      index.html

      <p class="yellow-background-text"> Here is the second sample of paragraph text.<p> 
      

      In this code snippet, you have added some text content with the <p> element and specified the yellow-background-text class. Save the file and reload it in your browser. You should have a webpage with two different sentences, the first one red and the second one with a yellow background:

      Webpage with text styled by two classes

      Note that you can add more than one class to an HTML tag. Try adding both classes to a single text element by adding the following line to your index.html file:

      index.html

      <p class="red-text yellow-background-text">Here is a third sample of text.</p>
      

      Note that the class names are only separated by a space. Save the file and reload it in the browser. You should receive something like this:

      IWebpage with text styled by three classes

      Your third line of text should now be styled according to the property values set in the red-text class and the yellow-background-text class and have a red font and yellow background.

      Adding CSS Classes to Images

      CSS classes can also be applied to other HTML elements, such as images. To explore using CSS classes for images, erase the content in your styles.css file and add the following code snippet:

      styles.css

      .black-img {
        border: 5px dotted black;
        border-radius: 10%;
      }
      
      .yellow-img {
        border: 25px solid yellow;
        border-radius: 50%;
      }
      
      .red-img {
        border: 15px double red;
      }
      

      Here you have created CSS rules for three different classes that can be applied to the HTML <img> tag. Before you move on, let’s briefly study what we’ve declared in each ruleset:

      • The first CSS rule declares that the class black-img should have a black, dotted border five pixels wide and a border-radius sized at 10%, which gives the element rounded corners.
      • The second CSS rule declares that the class yellow-img should have a yellow, solid border 25 pixels wide and a border-radius sized at 50%, which gives the element a circular shape.
      • The third CSS rule declares that the class red-img should have a red, double border 15 pixels wide. You have not set a border-radius, so the border will conform to the element’s shape.

      Save the styles.css file. Then erase everything from your index.html file (except for the first line of code: <link rel="stylesheet" href="https://www.digitalocean.com/community/tutorials/css/styles.css">) and add the following code snippet:

      index.html

      <img src="https://css.sammy-codes.com/images/small-profile.jpeg" class="black-img">
      <img src="https://css.sammy-codes.com/images/small-profile.jpeg" class="yellow-img">
      <img src="https://css.sammy-codes.com/images/small-profile.jpeg" class="red-img">
      

      Each of these three lines of HTML code add an image to the HTML document and assign it one of the three classes you just added to the styles.css file. Note that you are sourcing the image from an online location. You can also use your own image by specifying the file path as instructed in our tutorial How To Add Images To Your Webpage With HTML.

      Save your index.html file and load it in the browser. You should receive something like this:

      Webpage with images styled with three classes

      Your webpage should now display three images, each styled with the different specifications of their assigned class.

      To continue exploring CSS classes, trying creating new classes with different rulesets and applying them to different types of HTML content. Note that properties and values specified in class declaration blocks will only work on elements that they are intended for. For example, a font-color declaration will not change the color of an image border. Likewise, a height declaration will not change the size of the font.

      Conclusion

      You have now explored how to create classes, assign them specific property values, and apply them to text and image content. You will return to using classes when you begin building the website in the second half of this tutorial series.

      In the next tutorial, you will create CSS ID selectors, which work similarly as class selectors with the exception of some unique features.



      Source link

      Comprendre les classes en JavaScript


      Introduction

      JavaScript est un langage basé sur des prototypes, et chaque objet en JavaScript possède une propriété interne cachée appelée [[Prototype]] qui peut être utilisée pour étendre les propriétés et les méthodes des objets. Vous pouvez en savoir plus sur les prototypes dans notre tutoriel Comprendre les prototypes et l’héritage en JavaScript.

      Jusqu’à récemment, les développeurs industriels utilisaient des fonctions de constructeur pour imiter un modèle de conception orienté objet en JavaScript. La spécification de langage ECMAScript 2015, souvent appelée ES6, a introduit des classes dans le langage JavaScript. Les classes en JavaScript n’offrent pas réellement de fonctionnalités supplémentaires, et sont souvent décrites comme fournissant du « sucre syntaxique » par rapport aux prototypes et à l’héritage, en ce sens qu’elles offrent une syntaxe plus propre et plus élégante. Comme d’autres langages de programmation utilisent des classes, la syntaxe des classes en JavaScript permet aux développeurs de passer plus facilement d’un langage à l’autre.

      Les classes sont des fonctions

      Une classe JavaScript est un type de fonction. Les classes sont déclarées avec le mot-clé class. Nous utiliserons la syntaxe d’expression de fonction pour initialiser une fonction et la syntaxe d’expression de classe pour initialiser une classe.

      // Initializing a function with a function expression
      const x = function() {}
      
      // Initializing a class with a class expression
      const y = class {}
      

      Nous pouvons accéder au [[Prototype]] d’un objet en utilisant la méthode Object.getPrototypeOf(). Utilisons cela pour tester la fonction vide que nous avons créée.

      Object.getPrototypeOf(x);
      

      Output

      ƒ () { [native code] }

      Nous pouvons également utiliser cette méthode sur la classe que nous venons de créer.

      Object.getPrototypeOf(y);
      

      Output

      ƒ () { [native code] }

      Les codes déclarés avec function et class renvoient tous deux une fonction [[Prototype]]. Avec les prototypes, toute fonction peut devenir une instance de constructeur en utilisant le mot-clé new.

      const x = function() {}
      
      // Initialize a constructor from a function
      const constructorFromFunction = new x();
      
      console.log(constructorFromFunction);
      

      Output

      x {} constructor: ƒ ()

      Cela s’applique également aux classes.

      const y = class {}
      
      // Initialize a constructor from a class
      const constructorFromClass = new y();
      
      console.log(constructorFromClass);
      

      Output

      y {} constructor: class

      Ces exemples de constructeurs de prototypes sont par ailleurs vides, mais nous pouvons voir comment, au-delà de la syntaxe, les deux méthodes aboutissent au même résultat final.

      Définir une classe

      Dans le tutoriel sur les prototypes et l’héritage, nous avons créé un exemple basé sur la création de personnages dans un jeu de rôle en mode texte. Continuons avec cet exemple pour modifier la syntaxe en la faisant passer des fonctions aux classes.

      Une fonction de constructeur est initialisée avec un certain nombre de paramètres, qui seraient attribués comme propriétés de this, en référence à la fonction elle-même. La première lettre de l’identifiant serait en majuscule par convention.

      constructor.js

      // Initializing a constructor function
      function Hero(name, level) {
          this.name = name;
          this.level = level;
      }
      

      Lorsque nous traduisons cela dans la syntaxe de classe, illustrée ci-dessous, nous constatons qu’elle est structurée de manière très similaire.

      class.js

      // Initializing a class definition
      class Hero {
          constructor(name, level) {
              this.name = name;
              this.level = level;
          }
      }
      

      Nous savons qu’une fonction de constructeur est censée être un plan d’objet par la capitalisation de la première lettre de l’initialiseur (qui est facultative) et par la familiarité avec la syntaxe. Le mot-clé class communique de manière plus directe l’objectif de notre fonction.

      La seule différence dans la syntaxe de l’initialisation est l’utilisation du mot-clé class au lieu de function, et l’attribution des propriétés à l’intérieur d’une méthode constructor ().

      Définir les méthodes

      La pratique courante avec les fonctions de constructeur est d’assigner les méthodes directement au prototype plutôt que dans l’initialisation, comme on le voit dans la méthode greet() ci-dessous.

      constructor.js

      function Hero(name, level) {
          this.name = name;
          this.level = level;
      }
      
      // Adding a method to the constructor
      Hero.prototype.greet = function() {
          return `${this.name} says hello.`;
      }
      

      Avec les classes, cette syntaxe est simplifiée, et la méthode peut être ajoutée directement à la classe. En utilisant la syntaxe simplifiée de définition de méthode introduite dans l’ES6, la définition d’une méthode est un processus encore plus concis.

      class.js

      class Hero {
          constructor(name, level) {
              this.name = name;
              this.level = level;
          }
      
          // Adding a method to the constructor
          greet() {
              return `${this.name} says hello.`;
          }
      }
      

      Examinons ces propriétés et ces méthodes en action. Nous allons créer une nouvelle instance de Hero en utilisant le mot-clé new, et lui attribuer quelques valeurs.

      const hero1 = new Hero('Varg', 1);
      

      Si nous imprimons plus d’informations sur notre nouvel objet avec console.log(hero1), nous pouvons voir plus de détails sur ce qui se passe avec l’initialisation de la classe.

      Output

      Hero {name: "Varg", level: 1} __proto__: ▶ constructor: class Hero ▶ greet: ƒ greet()

      Nous pouvons voir dans la sortie que les fonctions constructor() et greet() ont été appliquées au __proto__, ou [[Prototype]] de hero1, et non directement comme méthode sur l’objet hero1. Si cela est évident lors de la création de fonctions de constructeur, ce n’est pas le cas lors de la création de classes. Les classes permettent une syntaxe plus simple et plus succincte, mais sacrifient une certaine clarté dans le processus.

      Étendre une classe

      L’une des caractéristiques avantageuses des fonctions de constructeur et des classes est qu’elles peuvent être étendues à de nouveaux plans d’objet basés sur le parent. Cela permet d’éviter la répétition du code pour des objets qui sont similaires mais qui nécessitent des caractéristiques supplémentaires ou plus spécifiques.

      De nouvelles fonctions de constructeur peuvent être créées à partir du parent en utilisant la méthode call(). Dans l’exemple ci-dessous, nous allons créer une classe de personnage plus spécifique appelée Mage, et lui attribuer les propriétés de Hero en utilisant call(), tout en ajoutant une propriété supplémentaire.

      constructor.js

      // Creating a new constructor from the parent
      function Mage(name, level, spell) {
          // Chain constructor with call
          Hero.call(this, name, level);
      
          this.spell = spell;
      }
      

      À ce stade, nous pouvons créer une nouvelle instance de Mage en utilisant les mêmes propriétés que celles de Hero, ainsi qu’une nouvelle propriété que nous avons ajoutée.

      const hero2 = new Mage('Lejon', 2, 'Magic Missile');
      

      En envoyant hero2 à la console, nous pouvons voir que nous avons créé un nouveau Mage basé sur le constructeur.

      Output

      Mage {name: "Lejon", level: 2, spell: "Magic Missile"} __proto__: ▶ constructor: ƒ Mage(name, level, spell)

      Avec les classes ES6, le mot-clé super est utilisé à la place de call pour accéder aux fonctions parents. Nous utiliserons extends pour renvoyer à la classe parent.

      class.js

      // Creating a new class from the parent
      class Mage extends Hero {
          constructor(name, level, spell) {
              // Chain constructor with super
              super(name, level);
      
              // Add a new property
              this.spell = spell;
          }
      }
      

      Nous pouvons maintenant créer une nouvelle instance Mage de la même manière.

      const hero2 = new Mage('Lejon', 2, 'Magic Missile');
      

      Nous allons imprimer hero2 sur la console et visualiser le résultat.

      Output

      Mage {name: "Lejon", level: 2, spell: "Magic Missile"} __proto__: Hero ▶ constructor: class Mage

      Le résultat est presque exactement le même, sauf que dans la construction de la classe, le [[Prototype]] est lié au parent, dans ce cas Hero.

      Vous trouverez ci-dessous une comparaison côte à côte de l’ensemble du processus d’initialisation, d’ajout de méthodes et d’héritage d’une fonction de constructeur et d’une classe.

      constructor.js

      function Hero(name, level) {
          this.name = name;
          this.level = level;
      }
      
      // Adding a method to the constructor
      Hero.prototype.greet = function() {
          return `${this.name} says hello.`;
      }
      
      // Creating a new constructor from the parent
      function Mage(name, level, spell) {
          // Chain constructor with call
          Hero.call(this, name, level);
      
          this.spell = spell;
      }
      

      class.js

      // Initializing a class
      class Hero {
          constructor(name, level) {
              this.name = name;
              this.level = level;
          }
      
          // Adding a method to the constructor
          greet() {
              return `${this.name} says hello.`;
          }
      }
      
      // Creating a new class from the parent
      class Mage extends Hero {
          constructor(name, level, spell) {
              // Chain constructor with super
              super(name, level);
      
              // Add a new property
              this.spell = spell;
          }
      }
      

      Bien que la syntaxe soit assez différente, le résultat sous-jacent est presque le même entre les deux méthodes. Les classes nous donnent un moyen plus concis de créer des plans d’objets, et les fonctions de constructeur décrivent plus précisément ce qui se passe sous le capot.

      Conclusion

      Dans ce tutoriel, nous avons découvert les similitudes et les différences entre les fonctions de constructeur JavaScript et les classes ES6. Les classes et les constructeurs imitent un modèle d’héritage orienté objet en JavaScript, qui est un langage d’héritage basé sur un prototype.

      Comprendre l’héritage des prototypes est primordial pour être un développeur JavaScript efficace. Il est extrêmement utile de se familiariser avec les classes, car les bibliothèques JavaScript populaires telles que React utilisent fréquemment la syntaxe class.



      Source link