You are viewing the Articles published in

Facilitating Reuse with TypeScript Generics

As is so often emphasized in my articles, facilitating reuse is perhaps one of the most import aspects of system design. For over a decade, languages such as Java, and C# have used generics as one of the primary mechanisms for reuse (I recall first hearing about them in C# back in 2005). Fortunately, Generics have been a key feature of TypeScript since the very beginning.

This article aims to briefly illustrate some of the main benefits of leveraging Generics in TypeScript to simplify implementations and maximize reuse.

Typescript Generics are a language feature that allow for providing reusable implementations based on type deferment. This means you can create a function, interface, or class with a placeholder for a type, thus affording the flexibility to use any type without losing the safety and robustness of type checking.

With Generics, types can be defined as an abstraction such that the types can be specified by clients based on their context. This affords the ability to allow implementations to adapt to the types specified while still ensuring type integrity is preserved.

Generics are defined within angle brackets <>, typically specified as “T” (for “Type”), where “T” is simply an arbitrary placeholder name which is used to denote and reference the type.

For example, consider the following function:

Using Generics, we can implement the function to accept a Generic Type rather than a fixed type:

In the above example, we define the generic within angle brackets, and reference the Type via the “T” value passed to the generic. Now, rather than only allowing strings to be logged, any type can be provided.

We can then use the generic by simply specifying the type when we invoke the function:

Note that in the above, we explicitly set the type to string; however, this isn’t necessary when the type can be inferred, in which case the type can be omitted if preferred:

We can also pass any type to the function’s generic as well:

Generics are not limited to functions. They can also be applied to interfaces and classes as well:

As can be seen in the above, just as with functions, we can define Generics for interfaces and classes using the same syntax.

TypeScript’s Generics also support more advanced features such as constraints, default types, and using multiple types, allowing for more precise and robust implementations.

Using Constraints
Using constraints in Generics allows for specifying requirements for the generic types. For instance, we can define a function which only operates on types that have a certain property or method.

Consider a getLength function which only accepts arguments that have a length property, ensuring the argument is array-like or a string:

Default Types
Default types in generics offer a way to specify a fallback type to be used when a type is not explicitly provided. This is quite useful for providing default implementations, while still affording the flexibility of allowing clients to override the default type.

For example, we can create a generic getData function that fetches data and returns a response of the provided type, or fallback to a default type if not provided:

Using Multiple Types
Multiple types can also be used in Generics, allowing for creating functions, interfaces, or classes that can handle more than one type of input, and / or return more than one type of output, offering greater flexibility and precision.

For instance, consider a simple merge function which combines two different objects into one, while preserving their distinct types:

We can also expand on this to include constraints to ensure the merge function only accepts parameters that are object-like:

TypeScript Generics offer a powerful way to build flexible, reusable, and type-safe solutions. By abstracting over types, Generics allow for providing implementations which can operate on a variety of data types, resulting in increased scalability and maintainability.

The Pipe Operator: A Glimpse into the Future of Functional JavaScript

In the dynamic landscape of JavaScript, the TC39 proposal for the Pipe Operator tends to stand out as an interesting progression in terms of streamlining function composition in a way increases readability, maintainability, and DX.

In this article, we dive a bit deeper into the realms of functional programming in JavaScript, and how upcoming language features such as the Pipe Operator aid in the ability to facilitate a more declarative approach to functional programming.

At its core, the Pipe Operator, denoted by (|>) introduces syntactic sugar for function composition, allowing developers to pass the result of an expression as an argument to a function. And, while the syntax may appear somewhat unfamiliar at first glance, this seemingly simple language feature harbors some rather profound implications for code clarity and maintainability.

Before diving into some examples, let’s first take a look at how functions are typically composed in JavaScript, and then touch on some of the drawbacks that result from these traditional approaches.

For instance, consider this simple example which demonstrates how one could compose three functions:

As can be seen in the above, composing functions together in this manner is cumbersome at best. Moreover, implementations such as this significantly lack in readability as they effectively obscure intent; that is, we simply want to end up with “abc”, but to do so requires an inversion of our thinking.

Of course, we can simplify things quite a bit by implementing a simple compose function (or utilizing a utility library, such as lodash/fp), which we can then leverage for composing functions in a more natural way:

With the above implementation, managing the composition of functions becomes easier?-?and we can also defer invoking the function to a later time. Yet, it still leaves much to be desired, especially in terms of maintainability. For instance, should we need to change the composition, the order of arguments must be changed proportionately.

Alternatively, developers may choose to bypass chaining altogether and opt for a temporary variable approach in order to simplify implementation and readability. For example:

While this is rather subjective, the use of temporary variables arguably creates unnecessary cognitive load as one must follow the order of assignments, and contend with temporary values which, if not implemented as constants, could lead to potential mutations, etc.

Considering the traditional approach to nested function calls which results in a right-ward drift that is challenging to read and understand, the Pipe Operator on the other hand turns this paradigm on its head, so to speak, by enabling left-to-right composition of functions which organically reflects our natural way of thinking and recognizing patterns, as can be seen in the following:

In the above example, expression is the value that is first passed to functionA, the result of which (i.e. the value returned from functionA) is then passed to functionB, and so on until the last function (functionC) returns, at which point the final value is assigned to result. The readability of this approach as compared to traditional function composition is self-evident, reducing cognitive load and making the flow of data much more apparent.

Given the previous examples, with the Pipe Operator, we can now simplify the implementation in a much more natural way:

The simplicity and utility of the Pipe Operator results in much more succinct expressions which in turn reduces the mental overhead of reading and understanding the implementations intent.

The practical applications of the Pipe Operator are vast, as they can be used to simplify compositions for everything from data processing pipelines to event handling flows.

For instance, consider a scenario where we need to process a dataset through a series of transformations. Using the Pipe Operator, we can accomplish this in a simple and concise manner:

With the streamlined syntax of the Pipe Operator, both intent and the flow of control become much clearer. In addition, maintainability is vastly improved as we can change the order of the processes with considerably less effort. For example, if we decide we want to enrich the results prior to normalizing, we simply just change the order accordingly as needed:

As we see, changing the order of invocations is rather simple, thus maintainability is vastly improved.

A particularly intriguing aspect of the Pipe Operator proposal is the inclusion of “Topic References”; a concept which increases expressiveness and the utility of the Pipe Operator by providing direct access to values via a topicToken.

Topic References allow for elegant handling of the current value within the pipeline, using a symbol (currently, %) as a placeholder reference to the value. This feature enables more complex operations and interactions with the piped value beyond that of simply passing the value as an argument to a function.

The main purpose of topic references is to enhance readability and flexibility for use-cases which involve multiple transformations or operations. By using a placeholder for the current value, developers can clearly express operations like method calls, arithmetic operations, and more complex function invocations directly on the value being piped through, without needing to wrap these operations in additional functions.

Consider a scenario where you’re processing a string to ultimately transform it into a formatted message. Without topic references, each step would require an additional function, even for simple operations. With topic references, however, the process becomes much more direct and readable:

One point to note regarding the topicToken is that it has not been finalized, thus the token is subject to change but will ultimately be one of the following: %, ^^, @@, ^, or #. Currently, @babel/plugin-proposal-pipeline-operator defaults to %, which can be configured to use one of the proposed topicTokens.

Through the use of topic references, the Pipe Operator proposal not only adheres to traditional functional programming principles, but also enhances developer experience by allowing for more intuitive and maintainable implementations. Features such as these represents a significant step forward in providing more declarative and expressive patterns in JavaScript.

The Pipe Operator proposal is currently in the pipeline for standardization, reflecting a collective effort within the JavaScript community to adopt functional programming paradigms. By facilitating a more declarative approach to coding, this proposal aligns with the language’s evolution towards offering constructs that support modern development practices.

Key benefits of the Pipe Operator include:

  • Enhanced Readability: Allows for a straightforward expression of data transformations, improving the readability of the code and making it more accessible to developers.
  • Reduced Complexity: Simplifies complex expressions that would otherwise require nested function calls or intermediate variables, thereby reducing the potential for errors.
  • A More Functional Paradigm: By promoting function composition, the Pipe Operator strengthens JavaScript’s capabilities as a language well-suited for functional programming.

As the JavaScript ecosystem continues to evolve, with TC39 proposals such as the Pipe Operator set to play an important role in shaping the future of the language, especially from a functional programming perspective.

While the proposal is still under consideration, its potential to enhance developer experience and promote functional programming principles is most certainly something to look forward to.

(Update: August, 2021, proposal has been moved to Stage 2)