Categories
JavaScript Vue

How to Create Vue Directive

Vue.js is an easy to use web app framework that we can use to develop interactive front end apps.

In this article, we’ll look at how to make our directives accept arguments and values to directives and use them in our code.

Dynamic Directive Arguments

We can pass in arguments and values to a directive and then get these values from the binding parameter.

For example, we can make a directive that takes an argument and value as follows:

src/index.js :

Vue.directive("position", {  
  bind(el, binding, vnode) {  
    const validPositions = ["relative", "fixed", "absolute"];  
    if (validPositions.includes(binding.arg)) {  
      el.style.position = binding.arg;  
      el.style.top = `${binding.value}px`;  
    }  
  }  
});

new Vue({  
  el: "#app",  
  data: {  
    message: "Hello"  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <p v-position:absolute="50">{{message}}</p>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, we defined a positiion directive that takes an argument, which is accessed in the bind hook by using the binding parameter.

The binding parameter has an arg property to get the argument, which is absolute in index.html .

binding.value gets us the value, which is 50 in index.html .

Then we get the element that the directive is bound to with el and then we set the positioning of the element via the values from the binding parameter.

Accepting arguments and values make directives much more flexible than without arguments.

Function Shorthand

If we only have code in the bind and update hooks, then we can just pass in a function with the same signature as those hooks.

For example, we can shorten the example above to:

Vue.directive("position", (el, binding, vnode) => {  
  const validPositions = ["relative", "fixed", "absolute"];  
  if (validPositions.includes(binding.arg)) {  
    el.style.position = binding.arg;  
    el.style.top = `${binding.value}px`;  
  }  
});

They both do the same thing.

Object Literals

We can pass in an object literal as the value of a directive if our directive needs to accept multiple values.

For example, we can write the following code to accept an object’s properties’ values in our directive:

src/index.js:

Vue.directive("position", (el, binding, vnode) => {  
  const validPositions = ["relative", "fixed", "absolute"];  
  const { position, top } = binding.value;  
  if (validPositions.includes(position)) {  
    el.style.position = position;  
    el.style.top = top;  
  }  
});

new Vue({  
  el: "#app",  
  data: {  
    message: "Hello"  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <p v-position="{ position: 'absolute', top: '50px' }">{{message}}</p>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

The code above accepts an object with the position and top property and we take the object’s values and then set them as the style of the p element which the position directive is bound to.

A directive’s value can be any valid JavaScript expression.

Conclusion

Vue directives can take arguments and values. We can get argument values from the binding parameter of the bind hook via the binding.arg property.

To get the value passed to a directive, we can get it from the bind hook’s binding parameter via the binding.value property.

If our directive only has bind or update hooks, then we can shorten it to a function, which has the same signature as the bind or update hooks.

Categories
JavaScript Vue

Vue Components — Dynamic Slot Names and Shorthands

Vue.js is an easy to use web app framework that we can use to develop interactive front end apps.

In this article, we’ll look at add dynamic slot names.

Dynamic Slot Names

Since Vue.js 2.6.0, we can pass in dynamic slot names with v-slot . For example, we can use it as follows:

src/index.js :

Vue.component("user", {  
  data() {  
    return {  
      user: {  
        firstName: "Joe",  
        lastName: "Smith"  
      }  
    };  
  },  
  template: `<p>  
    <slot v-bind:user="user" name='first-name'></slot>  
    <slot v-bind:user="user" name='last-name'></slot>  
  </p>`  
});

new Vue({  
  el: "#app",  
  data: {  
    firstname: "first-name",  
    lastname: "last-name"  
  }  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <user>  
        <template v-slot:[firstname]="{ user }">  
          {{ user.firstName }}  
        </template>  
        <template v-slot:[lastname]="{ user }">  
          {{ user.lastName }}  
        </template>  
      </user>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, firstname and lastname in data are set as the names of the slots in our root template.

Named Slots Shorthand

v-slot has a shorthand. We can shorten v-slot: to the symbol # . For example, we can rewrite our example as follows:

src/index.js :

Vue.component("user", {  
  data() {  
    return {  
      user: {  
        firstName: "Joe",  
        lastName: "Smith"  
      }  
    };  
  },  
  template: `<p>  
    <slot v-bind:user="user" name='first-name'></slot>  
    <slot v-bind:user="user" name='last-name'></slot>  
  </p>`  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <user>  
        <template #first-name="{ user }">  
          {{ user.firstName }}  
        </template>  
        <template #last-name="{ user }">  
          {{ user.lastName }}  
        </template>  
      </user>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

For referencing the default slot, we have to write #default . For example, we write:

src/index.js :

Vue.component("user", {  
  data() {  
    return {  
      user: {  
        firstName: "Joe",  
        lastName: "Smith"  
      }  
    };  
  },  
  template: `<p>  
    <slot v-bind:user="user"></slot>      
  </p>`  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <user>  
        <template #default="{ user }">  
          {{ user.firstName }} {{ user.lastName }}  
        </template>  
      </user>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

Use with v-for

We can add a slot to provided fallback content in lists and allow us to change the content in the parent. For example, we can write the following:

src/index.js :

Vue.component("people", {  
  data() {  
    return {  
      persons: [  
        { name: "Joe", isAdult: true },  
        { name: "Jane", isAdult: false }  
      ]  
    };  
  },  
  template: `  
    <ul>    
      <li v-for='person of persons'>  
        <slot v-bind:person="person"></slot>      
      </li>  
    </ul>  
  `  
});

new Vue({  
  el: "#app"  
});

index.html :

<!DOCTYPE html>  
<html>  
  <head>  
    <title>App</title>  
    <meta charset="UTF-8" />  
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>  
  </head>  
  <body>  
    <div id="app">  
      <people v-bind:persons="persons">  
        <template #default="{ person }">  
          {{person.name}}  
          <span v-if="person.isAdult"> - Adult</span>  
          <span v-else> - Child</span>  
        </template>  
      </people>  
    </div>  
    <script src="src/index.js"></script>  
  </body>  
</html>

In the code above, we have the people component with persons data. We rendered persons with v-for in the people component and make the entries available via v-bind :

<slot v-bind:person="person"></slot>

Then in the root template, we have:

<people v-bind:persons="persons">  
  <template #default="{ person }">  
    {{person.name}}  
    <span v-if="person.isAdult"> - Adult</span>  
    <span v-else> - Child</span>  
  </template>  
</people>

to get person from people and then provide a template to display the entries as we wish without modifying the people component.

Conclusion

Since Vue 2.6.0, we can use dynamic slot names instead of hard-coded ones.

Also, the shorthand for named slots is # . For default slots, we always have to write #default .

Finally, we can use it with v-for to let us define a template that can be used with each entry. We use v-bind on the entry of the array instead of the whole array and then we can access that data in the parent component and add a templarte to display data the way we like by defining it in the parent.

Categories
JavaScript

JavaScript Events Handlers — Metadata and Ajax Events

In JavaScript, events are actions that happen in an app. They are triggered by various things like inputs being entered, forms being submitted, changes in an element like resizing, or errors that happen when an app is running, etc. We can assign an event handler to take action on these events.

Events that happen to DOM elements can be handled by assigning an event handler to properties of the DOM object for the corresponding events. In this article, we will look at the onloadedmetadata property of a media DOM element, and the onloadend property of an XmlHttpRequest object.

onloadedmetadata

The onloadedmetadata property of a media DOM element lets us set an event handler function that’s run when the loademetadata event is fired. Whenever media metadata is loaded this event will fire.

For example, we can use it by first adding a video tag to our HTML code:

<video src='https://sample-videos.com/video123/mp4/240/big_buck_bunny_240p_30mb.mp4'></video>

Then in the corresponding JavaScript code, we can set an event handler function to the onloadedmetadata property as we do in the following code:

const video = document.querySelector('video');
video.onloadedmetadata = (e) => {  
  console.log(e);   
}

Then we get the video’s metadata in the srcElement property of the e parameter, which is an Event object. We get something like:

clientHeight: 240  
clientLeft: 0  
clientTop: 0  
clientWidth: 320  
contentEditable: "inherit"  
controls: false  
controlsList: DOMTokenList [value: ""]  
crossOrigin: null  
currentSrc: "https://sample-videos.com/video123/mp4/240/big_buck_bunny_240p_30mb.mp4"  
currentTime: 0  
dataset: DOMStringMap {}  
defaultMuted: false  
defaultPlaybackRate: 1  
dir: ""  
disablePictureInPicture: false  
disableRemotePlayback: false  
draggable: false  
duration: 368.2  
elementTiming: ""  
ended: false  
enterKeyHint: ""  
error: null  
firstChild: null  
firstElementChild: null  
height: 0  
hidden: false  
id: ""  
innerHTML: ""  
innerText: ""  
inputMode: ""  
isConnected: true  
isContentEditable: false  
lang: ""  
lastChild: null  
lastElementChild: null  
localName: "video"  
loop: false  
mediaKeys: null  
muted: false  
namespaceURI: "[http://www.w3.org/1999/xhtml](http://www.w3.org/1999/xhtml)"  
networkState: 1  
nextElementSibling: script  
nextSibling: text  
nodeName: "VIDEO"  
nodeType: 1  
nodeValue: null  
nonce: ""  
offsetHeight: 240  
offsetLeft: 8  
offsetParent: body  
offsetTop: 8  
offsetWidth: 320  
...  
outerHTML: "<video src="https://sample-videos.com/video123/mp4/240/big_buck_bunny_240p_30mb.mp4"></video>"  
outerText: ""  
ownerDocument: document  
parentElement: body  
parentNode: body  
part: DOMTokenList [value: ""]  
paused: true  
playbackRate: 1  
played: TimeRanges {length: 0}  
playsInline: false  
poster: ""  
prefix: null  
preload: "metadata"  
previousElementSibling: null  
previousSibling: text  
readyState: 4  
remote: RemotePlayback {state: "disconnected", onconnecting: null, onconnect: null, ondisconnect: null}  
scrollHeight: 240  
scrollLeft: 0  
scrollTop: 0  
scrollWidth: 320  
seekable: TimeRanges {length: 1}  
seeking: false  
shadowRoot: null  
sinkId: ""  
slot: ""  
spellcheck: true  
src: "https://sample-videos.com/video123/mp4/240/big_buck_bunny_240p_30mb.mp4"  
srcObject: null  
style: CSSStyleDeclaration {alignContent: "", alignItems: "", alignSelf: "", alignmentBaseline: "", all: "", …}  
tabIndex: -1  
tagName: "VIDEO"  
textContent: ""  
textTracks: TextTrackList {length: 0, onchange: null, onaddtrack: null, onremovetrack: null}  
title: ""  
translate: true  
videoHeight: 240  
videoWidth: 320  
volume: 1  
webkitAudioDecodedByteCount: 10995  
webkitDecodedFrameCount: 4  
webkitDisplayingFullscreen: false  
webkitDroppedFrameCount: 0  
webkitSupportsFullscreen: true  
webkitVideoDecodedByteCount: 37638

Some useful metadata include the videoHeight, which tells us the height of the video in pixels, videoWidth, which tells us the width of the video in pixels, and duration which tells us the length of the video in seconds. duration is also available audio elements.

onloadend

The onloadend property of an XMLHttpRequest object let us assign an event handler to it which is run whenever the loadend event is fired. The loadend event is fired whenever a request is completed, regardless of whether the request is completed successfully or not. If it’s successful then this event will be fired after the load event. Otherwise, it’ll fire after the abort or error events.

For example, we can assign an event handler to the onloadend event as we do in the following code:

const loadButtonSuccess = document.querySelector('.load.success');  
const loadButtonError = document.querySelector('.load.error');  
const loadButtonAbort = document.querySelector('.load.abort');  
const log = document.querySelector('.event-log');

function handleLoadEnd(e) {  
    log.textContent = log.textContent + `${e.type}: ${e.loaded} bytes transferred\n`;  
}

function addListeners(xhr) {  
  xhr.onloadend = handleLoadEnd;  
}

function get(url) {  
  log.textContent = ''; const xhr = new XMLHttpRequest();  
  addListeners(xhr);  
  xhr.open("GET", url);  
  xhr.send();  
  return xhr;    
}

loadButtonSuccess.addEventListener('click', () => {  
    get('https://jsonplaceholder.typicode.com/todos/1');  
});

loadButtonError.addEventListener('click', () => {  
    get('https://somewhere.org/i-dont-exist'));  
});

loadButtonAbort.addEventListener('click', () => {  
    get('https://jsonplaceholder.typicode.com/todos/1').abort();  
});

In the code above, we have 3 buttons that run the click event handler whenever each button is clicked. The ‘Load (success)’ button will run the get function when it’s clicked. We’ll pass in a valid URL in the call for the get function. The click handling for the ‘Load (success)’ button is done by the following block:

loadButtonSuccess.addEventListener('click', () => {  
    get('https://jsonplaceholder.typicode.com/todos/1');  
});

The JSONPlaceholder has a test API that can serve any URL since it hosts a fake API so we can load it and not get any errors. Likewise, we have buttons for load a URL that’ll give an error, and another button to load a valid URL but then we abort the request. Once the XmlHttpRequest is finished, then the function we assigned to the onloadend event handler, which is the handleLoadEnd function, will be run.

The handleLoadEnd function has one parameter, which is an Event object with some data about the request that’s finished. In the function, we get the value of the type property, which has the event type that’s fired, which should be loadend . Also, we get the value of the loaded property which has the number of bytes of data that’s been loaded.

Then in the HTML code, we add the elements listed in the querySelector calls above:

<div class="controls">  
    <input class="load success" type="button" name="xhr" value="Load (success)" />  
    <br>  
    <input class="load error" type="button" name="xhr" value="Load (error)" />  
    <br>  
    <input class="load abort" type="button" name="xhr" value="Load (abort)" />  
</div>
<textarea readonly class="event-log"></textarea>

We have 3 buttons to click on to load the successful HTTP request, an HTTP request with a non-existent URL, and an aborted HTTP request respectively. Then we display the event that’s fired and the number of bytes loaded.

The onloadedmetadata property of a media DOM element lets us set an event handler function that’s run when the loademetadata event is fired. Whenever media metadata is loaded this event will fire. We can get the metadata that’s loaded from the srcElement property of the event parameter of the event handler function.

The onloadend property of an XMLHttpRequest object let us assign an event handler to it which is run whenever the loadend event is fired. The loadend event is fired whenever a request is completed, regardless of whether the request is completed successfully or not. If it’s successful then this event will be fired after the load event. Otherwise, it’ll fire after the abort or error events. We can handle this event by setting the onloadend property of an XMLHttpRequest object. The event handler function should have an event parameter which is an event object that has the type property, which has the event type that’s fired, which should be loadend . Also, we get the value of the loaded property which has the number of bytes of data that’s been loaded.

Categories
JavaScript

JavaScript Events Handlers — Keyboard and Load Events

In JavaScript, events are actions that happen in an app. They’re triggered by various things like inputs being entered, forms being submitted, and changes in an element like resizing, or errors that happen when an app is running, etc. We can assign an event handler to respond to these events.

Events that happen to DOM elements can be handled by assigning a handler to properties of the DOM object for the corresponding events. In this article, we’ll look at how to use the onkeydown and onkeypress properties of an editable input element, and the onload property for any DOM element. We will also look at the onloadeddata property for media elements like audio and video .

onkeydown

We can set an event handler function to the onkeydown property of an input DOM element to handle the keydown event. The keydown event is fired when any key is pressed down regardless of whether they produce a character value. The keydown event provides a code indicating which key is pressed, while the keypress event provides the character which is entered. For example, lowercase ‘a’ will be reported as keycode 65 with keydown , but the character code 97 will be reported by keypress . keypress is deprecated, so it shouldn’t be used in production code.

In Firefox, since version 65, the keydown event is also fired during IME composition, to improve cross-browser compatibility for Chinese, Japanese and Korean users. We can ignore the keydown event during IME composition, we can check the isComposing property of the event object that’s provided by the keydown event handler. For example, we can write:

const input = document.querySelector('input');

input.onkeydown =  event => {   
  if (event.isComposing || event.keyCode === 229) {  
    return;  
  }  
};

Whenever, we type in Chinese, Japanese or Korean, the isComposing property will have the value true and the keyCode property will have the value 229.

We can also use it to log the value of the key code that’s pressed by the user on the keyboard. We can first put the following HTML code:

<input type="text" id="input" required>  
<p id="log"></p>

Then in the corresponding JavaScript code, we can write the following code to attach the event handler function for the keydown event by setting the onkeydown property of our input element. The event handler function will log the key code of the key that was pressed.

const input = document.querySelector('input');  
const log = document.getElementById('log');

input.onkeydown =  e => {   
 log.textContent += ` ${e.code}`;  
};

We should get something like:

KeyE KeyR KeyF KeyG KeyT KeyG KeyT KeyG KeyH KeyH KeyF KeyV KeyG KeyB KeyG KeyB

in the element with the ID log .

onkeyup

The onkeyup property of an input element let us attach an event handler function to handle the keyup event. The keyup event is fired when the user releases a key that’s been previously pressed.

The keyup event provides a code indicating which key is pressed, while the keypress event provides the character which is entered. For example, lowercase ‘a’ will be reported as keycode 65 with keyup, but the character code 97 will be reported by keypress .

In Firefox, since version 65, the keyup event is also fired during IME composition, to improve cross-browser compatibility for Chinese, Japanese and Korean users. We can ignore the keyup event during IME composition, we can check the isComposing property of the event object that’s provided by the keyup event handler. For example, we can write:

const input = document.querySelector('input');

input.onkeyup =  event => {   
  if (event.isComposing || event.keyCode === 229) {  
    return;  
  }  
};

We can also use it to log the value of the key code that’s pressed by the user on the keyboard. We can first put the following HTML code:

<input type="text" id="input" required>  
<p id="log"></p>

Then in the corresponding JavaScript code, we can write the following code to attach the event handler function for the keydown event by setting the onkeydown property of our input element. The event handler function will log the key code of the key that was pressed.

const input = document.querySelector('input');  
const log = document.getElementById('log');input.onkeyup =  e => {   
 log.textContent += ` ${e.code}`;  
};

We should get something like:

KeyD KeyF KeyG KeyT KeyH KeyG KeyT

onload

The onload property of a DOM element let us set an event handler function to it to handle the load event, which is fired when any element is being loaded. It’s fired at the end of the document loading process. All of the objects in the documents should be in the DOM when the load event is fired, including all images, scripts, links and sub-frames.

There’re also the DOMContentLoaded and DOMFrameContentLoaded , which are fired after the DOM for the page has been loaded but don’t wait for the other resources to finishing loading.

For example, we can use it to run some code after the window or an img element loads by first writing the HTML code:

<!DOCTYPE html>  
<html>  
  <head>  
    <title>Onload</title>  
  </head>  
  <body>  
    <div>Text</div>  
    <img  
      src="https://images.unsplash.com/photo-1503066211613-c17ebc9daef0?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1500&q=80"  
    />  
    <script src="main.js"></script>  
  </body>  
</html>

Then we can write the corresponding JavaScript code:

const img = document.querySelector("img");

img.onload = () => {  
  console.log("img loaded");  
};

window.onload = () => {  
  console.log("window loaded");  
};

Then we should see:

img loaded  
window loaded

When we load the page.

onloadeddata

The loadeddata event is fired when the first frame of the media has finished loading. It’s applicable to media elements like audio and video . To handle this event, we can set the onloadeddata property of a media element to run an event handler function when this event is fired.

For example, we can also set the onloadeddata property the following code. First, we add the HTML code for the video element:

<video src='https://sample-videos.com/video123/mp4/240/big_buck_bunny_240p_30mb.mp4'></video>

Then in the corresponding JavaScript code, we can set an event handler function with the following code:

const video = document.querySelector('video');

video.onloadeddata = () => {  
 console.log('video loaded');  
}

The event handler function also has an event object as the first parameter. We can add an event parameter as we do in the following code:

const video = document.querySelector('video');

video.onloadeddata = (event) => {  
 console.log(event);  
}

Which gets us something like:

bubbles: false  
​cancelBubble: false  
​cancelable: false  
​composed: true  
​currentTarget: null  
​defaultPrevented: false  
​detail: 0  
​eventPhase: 0  
​explicitOriginalTarget: <html>  
​isTrusted: true  
​layerX: 0  
​layerY: 0  
​originalTarget: HTMLDocument https://fiddle.jshell.net/_display/
​rangeOffset: 0  
​rangeParent: null  
​relatedTarget: null  
​returnValue: true  
​srcElement: HTMLDocument https://fiddle.jshell.net/_display/
​target: HTMLDocument https://fiddle.jshell.net/_display/
​timeStamp: 1463  
​type: "focus"  
​view: Window https://fiddle.jshell.net/_display/
​which: 0

The output above is the properties and the corresponding values of the Event object. To see more details about the Event object, we can look at the previous articles.

We can set an event handler function to the onkeydown property of an input DOM element to handle the keydown event. The keydown event is fired when any key is pressed down regardless of whether they produce a character value. The keydown event provides a code indicating which key is pressed. The onkeyup property of an input element let us attach an event handler function to handle the keyup event. The keyup event is fired when the user releases a key that’s been previously pressed. The event object provides the same information as the keydown event handler.

The onload property of a DOM element let us set an event handler function to it to handle the load event, which is fired when any element is being loaded. It’s fired at the end of the document loading process. All of the objects in the documents should be in the DOM when the load event is fired, including all images, scripts, links and sub-frames.

The loadeddata event is fired when the first frame of the media like audio and video has finished loading. To handle this event, we can set the onloadeddata property of a media element to run an event handler function when this event is fired.

Categories
JavaScript TypeScript

Introduction to TypeScript Generics — Classes

One way to create reusable code is to create code that lets us use it with different data types as we see fit. TypeScript provides the generics construct to create reusable components where we can work with a variety of types with one piece of code. This allows devs to use these components by putting in their own types. In this article, we’ll look at the many ways to define generic classes and also validate properties by defining interfaces and the extends keyword and also force the code to only accept property names that are in an object by using the keyof keyword.

Defining a Generic Class

To define generic classes in TypeScript, we put the generic type marker in between the <> after the class name. We can have more than one type marker separated by a comma. Also, we can use the same type marker to mark the type of the function to let us change parameter and return type of methods within our class. For example, we can write the following code to define a generic class with a single generic type marker:

class GenericPerson<T> {    
  name: T;  
  getName: (name: T) => T;  
}let person = new GenericPerson<string>();  
person.name = 'Jane';  
person.getName = function(x) { return x; };

In the code above, we passed in the string type inside the <> . We can also change to any other type we want like number, boolean, or any interface like in the following code:

class GenericPerson<T> {    
  name: T;  
  getName: (name: T) => T;  
}let person = new GenericPerson<number>();  
person.name = 2;  
person.getName = function(x) { return x; };

We can also insert multiple types within the same class as in the following code:

class GenericPerson<T, U> {    
  name: T;  
  getName: (name: U) => U;  
}let person = new GenericPerson<string, number>();  
person.name = 'Jane';  
person.getName = function (x) { return x; };  
person.getName(2);

In the code above, T and U may or may not be different types. As long as we put them both in and that we pass in and assign data with the types we specified in the class definition, then the TypeScript compiler will accept it. The examples above are only useful for a limited amount of cases since we can’t reference any properties in our class definition since the TypeScript compiler doesn’t know what properties T or U contains.

To make the TypeScript compiler aware of the properties that may be used in our class definition, we can use an interface to list all the properties that can be used with the class and the use the extends keyword after the generic type marker to denote the that type may have the list of properties that are listed in the interface. For example, if we want to have different complex types in our class methods, we can write something like the following code:

interface PersonInterface {  
  name: string;  
  age: number;  
}

interface GreetingInterface {  
  toString(): string;  
}

class GenericPerson<T extends PersonInterface, U extends GreetingInterface> {    
  person: T;  
  greet(greeting: U): string {  
    return `${greeting.toString()} ${this.person.name}. You're ${this.person.age} years old`;  
  }  
}

let jane = new GenericPerson();  
jane.person = {  
  name: 'Jane',  
  age: 20  
};  
console.log(jane.greet('Hi'));

In the code above, we defined 2 interfaces, the PersonInterface and the GreetingInterface to denote the properties that can be referenced with T and U respectively. In the PersonInterface, we have the name and age properties listed so we can reference these properties for data with type T. For the data with U type we can call the toString method on it. Therefore, in our greet method, we can call toString on greeting since it has the U type and since this.person has the T type, we can get the name and age properties from it.

Then after the class definition, we can instantiate the class and then set values for the name and age properties on the jane object that we created. Then when we run console.log on jane.greet(‘Hi’) then we should see ‘Hi Jane. You’re 20 years old’ since we set the values for jane.person.

We can also put in the types explicitly when we instantiate the object to make the types more clear. Instead of what we have above, we can change it slightly and write the following code:

interface PersonInterface {  
  name: string;  
  age: number;  
}

interface GreetingInterface {  
  toString(): string;  
}

class GenericPerson<T extends PersonInterface, U extends GreetingInterface> {    
  person: T;  
  greet(greeting: U): string {  
    return `${greeting.toString()} ${this.person.name}. You're ${this.person.age} years old`;  
  }  
}

let jane = new GenericPerson<PersonInterface, string>();  
jane.person = {  
  name: 'Jane',  
  age: 20  
};  
console.log(jane.greet('Hi'));

The only difference is that we add the <PersonInterface, string> after new GenericPerson. Note that we can add interfaces or primitive types in between the brackets. TypeScript only cares that the type has the methods listed in the interface we defined. Now that the types are constrained by these generic types, we don’t have to worry about referencing any unexpected properties. For example, if we reference something that doesn’t exist like in the following code:

interface PersonInterface {  
  name: string;  
  age: number;  
}

interface GreetingInterface {  
  toString(): string;  
}

class GenericPerson<T extends PersonInterface, U extends GreetingInterface> {    
  person: T;  
  greet(greeting: U): string {  
    return `${greeting.foo()} ${this.person.name}. You're ${this.person.age} years old`;  
  }  
}

let jane = new GenericPerson<PersonInterface, string>();  
jane.person = {  
  name: 'Jane',  
  age: 20  
};  
console.log(jane.greet('Hi'));

We would get “Property ‘foo’ does not exist on type ‘U’.(2339)” since we didn’t list foo in our GreetingInterface .

Constraining Retrieval of Object Properties

TypeScript also provides a way to let us get the properties of an object safely with generics. We can use the keyof keyword to constrain the values of the than an object can take on to the key names of another object. For example, we can use the keyof keyword like in the following code:

function getProperty<T, K extends keyof T>(obj: T, key: K) {  
  return obj[key];  
}

let x = { foo: 1, bar: 2, baz: 3 };console.log(getProperty(x, "foo"));

In the code above, we constrain the generic marker K to only accept the keys of whatever object is passed in to obj as the valid values of key since K has the extends keyof T marker after K . This means that whatever keys the T type has, then the keys are valid values for K . With code like the ones above, we don’t have to worry about getting values for properties that don’t exist. So if we pass in a key name that doesn’t exist in obj , like in the following code:

getProperty(x, "a");

Then the TypeScript compiler will reject the code and give the error message “Argument of type ‘“a”’ is not assignable to parameter of type ‘“foo” | “bar” | “baz”’.(2345)“. This means that only 'foo' , 'bar' and 'baz' are valid values for key since it has the type K , which has the extends keyof T marker after it to constrain the valid uses to be the key names of obj .

We can define a generic class easily with TypeScript. To define generic classes in TypeScript, we put the generic type marker in between the <> after the class name. We can have more than one type marker separated by a comma. Also, we can use the same type marker to mark the type of the function to let us change parameter and return type of methods within our class. Also, we can use the extends keyword to define the properties that can be referenced with our data marked by generic types. We can use the keyof keyword to constrain the values of the than an object can take on to the key names of another object.