- 3 years ago
- Afaq Arif
- 3,820 Views
-
4
In this section, we have discussed Generics in TypeScript with examples in detail.
In software engineering, building components play a major role as they are reusable with well-defined and consistent APIs. Such components can work on the data of today as well as the data of tomorrow. It gives you the most flexible capabilities for building up large software systems.
Generics are one of the main tools in the toolbox for creating reusable components in languages like C# and Java. Such components can work over a variety of characters rather than a single one. This allows users to use these components and use their own types.
Hello World of Generics
To start off, let’s do the “hello world” of generics: the identity function. The identity function is a function that will return back whatever is passed in. You can think of this in a similar way to the echo
command.
We need to give the identity function a specific type without generics.
<code>function identity(arg: number): number { return arg;}</code>
Or, we can state the identity function using the any
type.
<code>function identity(arg: any): any { return arg;}</code>
Using any
is absolutely generic here causing the function to accept any and all types for the type of arg
. We will miss the information about what that type was when the function returns. If we pass in a number, the only information we have is that any type can return.
Rather, we need a way of obtaining the type of the argument in such a way that we can also use it to show what is being returned. Here, we will use a type variable, a special kind of variable that works on types rather than values.
<code>function identity<T>(arg: T): T { return arg;}</code>
We’ve now added a type variable T
to the identity function. This T
allows us to take the type the user provides (e.g. number
), so that we can use that information later. Here, we use T
again as the return type. We can now see the same type is used for the argument and the return type on review. It allows us to traffic that type of information on one side of the function and out the other.
We can say that this version of the identity
function is generic, as it works over a variety of types. Unlike using any
, it’s also just as true(ie, it doesn’t lose any information) as the first identity
function that used numbers for the argument and return type.
We can call the generic identity function in one of two ways after it is written. The first way is to pass all of the arguments, including the type argument, to the function.
<code>let output = identity<string>("myString");// ^ = let output: string</code>
We clearly set T
to be string
as one of the arguments to the function call here. It is shown using the <>
around the arguments rather than ()
.
The second way is perhaps the most common. Here we use type argument inference — that is, we want the compiler to set the value of T
for us automatically based on the type of the argument we pass in:
<code>let output = identity("myString");// ^ = let output: string</code>
Keep in mind that we didn’t have to directly pass the type in the angle brackets (<>
); the compiler just looked at the value "myString"
, and set T
to its type. Type argument result helps us to keep code shorter and more readable. But you may need to clearly pass in the type arguments as we did in the previous example when the compiler fails to understand the type, as may happen in more complex examples.
Working with Generic Type Variables
When you start using generics, you’ll see that when you create generic functions like identity
, the compiler will ask you to use any generically typed parameters in the body of the function correctly. That is, that you really use these parameters as if they can be any and all types.
Let’s take our identity
function from earlier.
<code>function identity<T>(arg: T): T { return arg;}</code>
What if we want to also log the length of the argument arg
to the comfort with each call? We might be influenced to write this.
<code>function loggingIdentity<T>(arg: T): T { console.log(arg.length);Property 'length' does not exist on type 'T'.Property 'length' does not exist on type 'T'. return arg;}</code>
When we do, the compiler will give us an error that we’re using the .length
member of arg
. It is clear as we have not mentioned that arg
has this member. As we said earlier that these types of variables stand in for any and all types, so someone using this function could have passed in an number
instead, which does not have a .length
member.
Let’s say that we’ve actually planned this function to work on arrays of T
rather than T
directly. Since we’re working with arrays, the .length
member should be available. We can explain this just like we will create arrays of other types.
<code>function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); return arg;}</code>
You can read the type of loggingIdentity
as “the generic function loggingIdentity
takes a type parameter T
, and an argument arg
which is an array of T
s, and returns an array of T
s.” If we passed in an array of numbers, we’d get an array of numbers back out, as T
would bind to number
. It allows us to use our generic type variable T as part of the types, we’re working with, rather than the whole type, giving us greater versatility.
We can alternatively write the sample example this way.
<code>function loggingIdentity<T>(arg: Array<T>): Array<T> { console.log(arg.length); // Array has a .length, so no more error return arg;}</code>
You may already be common with this style of type from other languages. In the next section, we’ll cover how you can create your own generic types like Array<T>
.
Generic Types
In previous sections, we created generic identity functions that worked over a variety of types. In this section, we’ll examine the type of functions themselves and how to create generic interfaces.
The type of generic functions is just like those of non-generic functions, with the type parameters listed first, similarly to function declarations.
<code>function identity<T>(arg: T): T { return arg;}
let myIdentity: <T>(arg: T) => T = identity;</code>
We can also use a different name for the generic type parameter in the type, so long as the number of type variables and how the type variables are used line up.
<code>function identity<T>(arg: T): T { return arg;}
let myIdentity: <U>(arg: U) => U = identity;</code>
We can also write the generic type as a call signature of an object literal type.
<code>function identity<T>(arg: T): T { return arg;}
let myIdentity: { <T>(arg: T): T } = identity;</code>
It guides us to writing our first generic interface. Let’s take the object written from the previous example and move it to an interface.
<code>interface GenericIdentityFn { <T>(arg: T): T;}
function identity<T>(arg: T): T { return arg;}
let myIdentity: GenericIdentityFn = identity;</code><a href="https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgOIRNYCCSATDMYMATwDERkBvAKGWQB4AVAPgAo4oBzALmSYCUfJgG4aAXxo0YAVxAIiAe0rAC4YiWbtOvfkP7U6yKBDAyolHWMk0ANqeQBbEvkIa+6TFGyv1pCsgAvMiqbqQiQA">Try</a>
In a similar example, we may want to move the generic parameter to be a parameter of the whole interface. It allows us to see what type(s) we are generic over (e.g. Dictionary<string>
rather than just Dictionary
). This makes the type parameter visible to all the other members of the interface.
<code>interface GenericIdentityFn<T> { (arg: T): T;}
function identity<T>(arg: T): T { return arg;}
let myIdentity: GenericIdentityFn<number> = identity;</code>
Notice that our example changes to be something slightly different. We now have a non-generic function signature that is a part of a generic type instead of stating a generic function. When we use GenericIdentityFn
, we also need to define the corresponding type argument (here: number
), completely locking in what the underlying call signature will use. It helps us to understand when to put the type parameter directly on the call signature and when to put it on the interface itself, helpful in defining what characters of a type are generic.
In addition to generic interfaces, we can also create generic classes. Note that it is not possible to create generic enums and namespaces.
Generic Classes
A generic class has a similar shape to a generic interface. Generic classes have a generic type parameter list in angle brackets (<>
) following the name of the class.
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T;}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y;};
This is an accurate use of the GenericNumber
class, but you may have noticed that nothing is limiting it to only use the number
type. Rather, we can use string
or even more complex objects.
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) { return x + y;};
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));
Just as with interface, putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.
As we covered in our section on classes, a class has two sides to its type: the static side and the instance side. Generic classes are only generic over their case side rather than their static side, so when working with classes, static members can not use the class’s type parameter.
Generic Constraints
If you remember from an earlier example, you may sometimes want to write a generic function that works on a set of types where you have some knowledge about what abilities that set of types will have. In our loggingIdentity
For example, we wanted to be able to access the .length
property of arg
, but the compiler could not prove that every type had a .length
property, so it warns us that we can’t make this assumption.
function loggingIdentity<T>(arg: T): T {
console.log(arg.length);
Property 'length' does not exist on type 'T'.Property 'length' does not exist on type 'T'. return arg;}
Rather than working with any and all types, we’d like to restrain this function to work with any and all types that also have the .length property. As long as the type has this member, we’ll allow it, but it’s required to have at least this member. To do so, we must list our requirements as a constraint on what T can be.
In order to do this, we’ll create an interface that defines our constraint. Here, we’ll create an interface that has a single .length
property and then we’ll use this interface and the extends
keyword to show our constraint.
interface Lengthwise { length: number;}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
// Now we know it has a .length property, so no more error return arg;
}
It will no longer work over any and all types as the generic function is now constrained.
loggingIdentity(3);
Argument of type 'number' is not assignable to parameter of type 'Lengthwise'. Argument of type 'number' is not assignable to parameter of type 'Lengthwise'
Preferably, we need to pass in values whose type has all the required properties.
loggingIdentity({ length: 10, value: 3 })
Using Type Parameters in Generic Constraints
You can state a type parameter that is constrained by another type parameter. For example, here we’d like to get a property from an object given its name. We’d like to assure you that we’re not accidentally taking a property that does not exist on the obj
, so we’ll place a constraint between the two types.
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key];}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");getProperty(x, "m");Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
Using Class Types in Generics
It is required to apply to class types by their constructor functions when producing factories in TypeScript using generics. For example,
function create<T>(c: { new (): T }): T { return new c();}
A more advanced example uses the prototype property to understand and constrain relationships between the constructor function and the instance side of class types.
class BeeKeeper { hasMask: boolean;}
class ZooKeeper { nametag: string;}
class Animal { numLegs: number;}
class Bee extends Animal { keeper: BeeKeeper;}
class Lion extends Animal { keeper: ZooKeeper;}
function createInstance<A extends Animal>(c: new () => A): A { return new c();}
createInstance(Lion).keeper.nametag;createInstance(Bee).keeper.hasMask;
A major part of software engineering is building components that not only have well-defined and consistent APIs but are also reusable. Components that are capable of working on the data of today, as well as the data of tomorrow, will give you the most flexible capabilities for building up large software systems.
In languages like C# and Java, one of the main tools in the toolbox for creating reusable components are generics, that is, being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
Hello World of Generics
To start off, let’s do the “hello world” of generics: the identity function. The identity function is a function that will return back whatever is passed in. You can think of this in a similar way to the echo command.
Without generics, we would either have to give the identity function a specific type:
function identity(arg: number): number {
return arg;
}
Or, we could describe the identity function using the any type:
function identity(arg: any): any {
return arg;
}
While using any is certainly generic in that it will cause the function to accept any and all types for the type of arg, we actually are losing the information about what that type was when the function returns. If we passed in a number, the only information we have is that any type could be returned.
- 3 years ago
- Afaq Arif
- 3,820 Views
-
4