Constraints in Generics

Learn via video courses
Topics Covered

Overview

JavaScript is a superset of TypeScript. Superset indicates that it adds functionalities to typescript which JavaScript already has in its libraries. TypeScript extends the functionalities and structures provided by JavaScript as a language by adding a few new features. JavaScript was launched as a client-side language.

Introduction

Statically typed languages must include generics because they enable programmers to send types as arguments to another type, function, or other structure. Making a component generic increases code flexibility makes components reusable, and eliminates duplication by enabling the component to accept and enforce the type that is handed in when the component is used.

To add type-safety to components that receive arguments and return values whose types won't be known until they're used later in your code, TypeScript fully supports generics. You will experiment with practical TypeScript generics examples in this lesson and learn how they are applied to functions, types, classes, and interfaces. Additionally, you will utilize generics to develop mapped types and conditional types, which will assist you in developing TypeScript components that can adapt to any required circumstances in your code.

Describing Constraints in Generics

We use the extends keyword to define constraints In generics.

The one-line update below demonstrates how to make our function generic in the right way.

Now that our argument type (HashId[]) is represented in two places, take note of our 'requirement' for it:

  • extends HashId as the restriction on T
  • to guarantee that we continue to receive an array, list: T[]

To specify an integrity constraint—a rule that limits the values in a database—use a constraint.

The six different integrity constraints in generics are briefly outlined here and in further detail in 'Semantics':

  • A NOT NULL constraint stops a database variable from becoming null.
  • A unique constraint forbids identical values from appearing in numerous rows in the same column or group of columns but permits certain values to be null.
  • In a single declaration, a main key constraint includes a NOT NULL constraint with a unique constraint. In other words, it doesn't allow values from being null and prevents multiple rows from having the same value in a single column or group of columns.
  • Values in one table must match values in another table in order to satisfy a foreign key constraint.
  • A check constraint demands that a value from the database satisfy certain criteria.
  • By definition, a REF column refers to an object in a relational table or in another object type. You can elaborate on the connection between the REF column and the object it refers to by using a REF constraint.

Constraints in generics can be defined syntactically in one of two ways:

  • Component of defining a certain column or characteristic. The inline specification is the term for this.
  • Part of the definition of the table. The out-of-line specification refers to this.

Constraints in generics on NOT NULL values must be expressed inline. The declaration of any additional limitations can be either inline or out of line.

What are Constraints in Generics?

Generic is supported by all statically-typed languages, including Typescript. We can create a flexible type signature using a generic that promotes function reuse. Without Generics, functions are limited to a single data type.

In order to obtain a high degree of flexibility while yet being able to securely assume some basic structure and behavior, generic constraints in generics enable us to express the ‘minimum requirement’ for a type param.

The function creates in the above example receives an array of strings and returns an array of strings. Depending on the use case, we may enhance this function by providing a sample a generic type that takes different data types in addition to strings.

We may require that our create function accept and return a particular type using generic.

The create function will also determine the type from the parameter if the type is omitted (['Test', 10] is converted to string | number). This looks fantastic, and we can reuse our function with various types of signatures.

But frequently, while constructing a generic function, we may already be aware of the fundamental principles at play, allowing us to provide a more specific type. This is referred to as a Generic Constraint in Typescript.

Types of Constraints in Generics

Interface Type Constraints in Generics

One or more interface types may be listed in a comma-separated list within a type parameter constraint.

The compiler will check at build time that a concrete type supplied as an input to a type function Object() implements the specified interface type if a type parameter is bound by an interface type (s).

Class Type Constraints in Generics

One class type or zero class types may confine a type parameter. This declaration implies that the constraint class must be assignment compatible with any concrete type supplied as an input to the constrained type param, much like with interface type constraints.

Class types can be passed in places where their ancestor types are needed according to the standard OOP compatibility requirements.

Constructor Constraints in Generics

The reserved term ‘function Object()’, when present in either zero or one instance, can confine a type parameter. In order for methods inside the generic type to create instances of the argument type using the argument type's default function Object() without knowing anything about the argument type itself, the real argument type must be a class that describes a default function Object() (a public parameterless function Object() ) (no minimum base type requirements).

You can blend ‘function Object()’ with the interface or class type constraints in generics in any order in a constraint declaration.

Value Type Constraint in Generics

This constraint indicates that the type parameter must be of the value type. We will get a compile-time error if we try to replace a non-value type for the type parameter. If we use the following code to declare the generic class, we will obtain a compile-time error if we try to substitute a reference type for the type argument.

The following line will result in a compilation error. Although string is a reference type, it is not acceptable to substitute a string as the type argument due to the value type requirement.

Class Constraints in Generics

In a nullable context, the class constraint states that the type parameter must be a non-nullable reference type. When a type parameter in a nullable context is a nullable reference type, the compiler issues a warning. A type parameter may have 0 or 1 instances of the reserved term ‘class’ as a constraint. Thus, a class type is required for the actual type.

Record Constraints in Generics

The reserved term ‘record’ may appear 0 or 1 times as a type parameter constraint. Thus, a value type must be the actual type (not a reference type). A ‘class’ or ‘function Object()’ constraint cannot be paired with a ‘record’ constraint.

Using Other Type Parameters Constraints in Generics

Generics are written in TypeScript code as <T>, where T stands for a type that has been handed in. You might interpret <T> as a generic type T. As placeholders for a type that will be defined when an instance of the structure is generated, T will act in this situation similarly to how parameters do in functions. Therefore, generic type parameters or simply type parameters are other names for the generic types defined inside angle brackets. A definition may have several generic types, such as <T, K, A>.

Functions, types, classes, and interfaces can all contain generics. For the time being, a function will be used as an example to show the fundamental syntax of generics. Simply applying certain restrictions to a type is what a generic constraint is. Consider the following generic function:

You'll see that null and undefined are permitted here; while this may be what we wanted, I'm certain that most of the time they are incorrect inputs. This problem may be fixed by imposing a restriction on our generic type that forbids empty values.

String, integer, array, and object are all subclasses of the object in Javascript, however undefined and null are not, hence they are not permitted. In the example above, T extends {} implies that T can be any type that is a subclass of {} (an object). This is how generic constraint syntax appears when the T type is extended.

We could use the argument's specific method anywhere in our function, but with generic, we can't be certain that it is present. Because of this, we need to further restrict our method such that it will only accept arguments with a particular signature.

Now that we've made some progress, let's see how we may use Typescript to carry out a more sophisticated restriction. Let's say we wanted to develop a function that would take a custom form and return the same shape,

The use of general restrictions in this situation is ideal. First, let's define our new type.

Three fields make up our own type: sample, test, and try. To resolve this, we may utilize the built-in type Partial in Typescript. The input can be either a whole set or a subset of Custom.

Perfect! No more, no less—exactly the form we requested is returned by our function. Keep in mind that the empty string " serves just as a placeholder value to complete the object's design; it has no functional effect (we can customize it though).

Generic Constraint on Constructor Function

We now have a more organic understanding of the benefits of generics and have observed a rather challenging use of them. I can remember having a hard time grasping the concept when I was first learning, so I want to develop that example by starting with simple functions. For the majority, the TypedList implementation likely already has a lot of sense, particularly if you come from a statically-typed language background. If the idea of Generics hasn't fully clicked yet, that's totally normal. Abstraction-related concepts in software may be famously challenging to absorb.

Let's start with basic functions and work our way up to being able to comprehend that example. With the Identity Function, we'll begin.

In mathematics, a function that directly translates its input to its output is known as an ‘identity function,’ for example, f(x) = x. You only get out what you put in. In JavaScript, we can express that as:

Or, more accurately:

The same type of system difficulties that we previously encountered return when we attempt to transfer this to TypeScript. The options include utilizing Generics, duplicating/overloading the function for each type (which violates DRY), or typing with any, which is generally not a good idea.

We may express the function as follows using the later choice:

Output:

Explanation

This function is declared to be generic by the syntax used. A Generic function allows us to supply an arbitrary type parameter in the same way as a function allows us to send an arbitrary input parameter into its argument list. identity(value: T): T and (value: T): T make up the identity element of the signature. In both instances, T states that the function in the issue will accept an argument of the generic type T. Our Generic placeholders can have any name, just like variables, although it's customary to start with a capital T (for ‘Type’) and work your way down the alphabet as necessary.

Assuming that T is a type, we additionally declare that we will accept one function parameter of type T with the name value and that our function will return a type of T. The signature only states that. Replace all the Ts in those signatures with string by mentally allowing T = string. Notice how nothing really amazing is occurring? See how close that is to how you use functions in a non-generic fashion every day?

Next, we 'pass in' the concrete type we want to use in the 'future' in the two log statements, exactly as we would a variable. The language used to describe our function is generic in the initial form of the <T> signature, which means that it may operate on generic types or types that will be specified later. That's because when we actually build the function, we have no idea what type the caller would want to use. However, the caller of the function is aware of the type or types, in this case, string and number, that they wish to operate with.

The original signature could essentially be thought of as identity(input: number):number at the point when we invoked the function with the number parameter. Additionally, the original signature could have just as well read identity(input: string): string at the point when we invoked the function with the string parameter. You can see every generic T being replaced with the specific type you specify at that precise instant when you make the call.

Note: Imagine having a log function written in a third-party library in this manner. The library creator has no clue what types the programmers who use the library will wish to use, so they make the function generic, basically postponing the requirement for specific types until they are known. To help you comprehend this procedure more intuitively, I want to emphasize that you should think of it similarly to the idea of passing a variable to a function. Right now, all we're doing is passing a type along.

Using Type Parameters in Generic Constraints

Any type that is subject to a type constraint on a generic type parameter must meet certain criteria in order to be used as a type argument for that type parameter. (For instance, it could need to implement a certain interface or be a certain class type or subclass of that class type.)

There are three different types of constraints:

  • Using T as sometype necessitates that sometype be a subtype of T.
  • T super sometype, which necessitates that T be a supertype of sometype
  • T must be comparable to sometype in order for T to be true. Saying both T as sometype and T super sometype is equivalent.

Conclusion

  • Your typescript codebases may be greatly enhanced by using type restrictions.
  • Use type parameters only if they are actually necessary. They increase complexity, and you should avoid doing so unless it is truly necessary.
  • Although not exactly the same as how it is used with type parameters, the extends keyword is used in object-oriented inheritance, and there is a conceptual relationship.
  • If a method overrides a different method that has constraints specified, it is required to re-declare those constraints, but only if the overriding method actually uses them.
  • The inclusion of generics in the typescript language is effective because it makes programming simpler and less prone to mistakes.
  • Generics enable the implementation of generic algorithms without adding any extra expense to our programs and, most crucially, ensure type correctness at compile time.
  • There are several benefits of employing constraints. One of the most significant advantages is that, in the case of an interface constraint, we know that a type implements a particular method, allowing us to invoke that method within the generic type.