Typescript is a beautiful language, it gives us a lot of confidence as developers, there are loads of awesome content that share Typescript's awesomeness, but today, we are going to take a different route. You want to build little projects with Typescript so you can solidify your knowledge, and that is why we are here right now.
Project Details
Our quote generator is no different from the ones you've probably built with Javascript or other tutorials have covered, our job here today is to replicate our Javascript code in Typescript.
So then, our app will talk to an API to fetch the quote, and then we can render the quote on our beautiful screen.
This is the first on the #JStoTSconversion
series I'd be covering here on my blog. So let's get started with what you need to have fun here.
Requirements
- HTML5
- CSS3
- Javascript
- Typescripts basics
If you have basic knowledge on these then you are good to go. Our next milestone is to get our project setup out of the way.
Install
typescript
globally withnpm i -g typescript
Structure and Initialization
Open your terminal, create a directory in your favourite location and cd
into it.
mkdir ts_quote_generator && cd ts_quote_generator
Next, add the tsconfig.json
file in the root.
touch tsconfig.json
Fill the new tsconfig.json
configuration file with the code snippet below:
{
"compilerOptions": {
"sourceMap": true,
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"ES2015"
],
"module": "CommonJS",
"outDir": "dist/",
"strict": true
}
}
We'll add a styles directory with a styles.css
file in it and an index.html
in the root.
- styles
- styles.css
- index.html
You can get the HTML file content from this gist and the stylesheet from here
Now let's get down to business.
Create an app.ts
file in the root of the project, for testing purposes, add this line of code:
console.log("app is connected");
Now open the terminal and run your first tsc
build command.
tsc
is short fortypescript compiler
.
Run this command: tsc --build tsconfig.json
. You can also run the tsc
command without the arguments, like so: tsc
. This should generate a new dist/
directory with two files.
Run the app and visit the browser console, we should see our message logging there.
With our typescript compilation working, we will shift attention to fleshing out the app logic.
// app.ts
const quoteContainer = document.getElementById('quote-container');
const loader = document.getElementById('loader');
const quoteText = document.getElementById('quote');
const authorText = document.getElementById('author');
const twitterBtn = document.getElementById('twitter');
const newQuoteBtn = document.getElementById('new-quote');
First, we register our DOM elements into our typescript file and hold them in memory. When making a request to the API for data, we need to show our loading state, we will write two helper functions for that (showContentLoader)
and (hideContentLoader)
;
// app.ts
const showContentLoader = () => {
if (loader && quoteContainer) {
loader.hidden = false;
quoteContainer.hidden = true;
}
}
const hideContentLoader = () => {
if (loader && quoteContainer) {
if (!loader.hidden) {
quoteContainer.hidden = false;
loader.hidden = true;
}
}
}
In both functions, you'd notice the line if (loader && quoteContainer) {
. This is because in our tsconfig.json
file we have specified the rule "strict": true
, so typescript will fail to build if we don't guard against null
values among other things.
But how did we come about the null
value?
When we try to get the HTMLElement
from the DOM
via getElementById()
or any other API, there are 2 possible scenarios;
- The element exists, and returns the corresponding data, or
- The element is unavailable at the moment and therefore will return
null
.
When we try to read the value loader.hidden
, we could, in fact, be doing null.hidden
, this would crash our app because the getElementById()
method returns a union of HTMLElement
or null
. James Henry talks more about this behaviour in his blog.
What have we gained?
Typescript enforces these checks to help us write quality and less buggy code. By checking for the availability of these elements, we save our app from crashing. Cool right? We will continue with this method throughout the code.
The getQuote Function
The getQuote()
is responsible for fetching our quotes from the API, we are expecting a response from that request, and hence, we will utilize Typescript's interface
to check for our data shape. Let's get the code;
interface QuoteData {
quoteAuthor: string;
quoteText: string;
quoteLink?: string;
senderLink?: string;
senderName?: string;
}
// Get quote from API
const getQuote = async () => {
showContentLoader();
const proxyUrl = 'https://cors-anywhere.herokuapp.com/'
const apiUrl = `https://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=json`;
try {
const response = await fetch(proxyUrl + apiUrl);
const data: QuoteData = await response.json();
if (authorText && quoteText) {
// default to annoynmous if there is no author
data.quoteAuthor === ''
? authorText.innerText = 'Anoynmous'
: authorText.innerText = data.quoteAuthor;
// Dynamically change text size
data.quoteText.length > 120
? quoteText.classList.add('long-quote')
: quoteText.classList.remove('long-quote');
quoteText.innerText = data.quoteText;
// show quote
hideContentLoader();
}
} catch (error) {
getQuote();
}
}
We ensure that the response coming from the API matches our interface
shape with this line const data: QuoteData = await response.json();
.
Tweet Function
Hook up the tweet function and the getQuote
function like so:
// Tweet quote
const tweetQuote = () => {
if (quoteText && authorText) {
const quote = quoteText.innerText;
const author = authorText.innerText;
const twitterUrl = `https://twitter.com/intent/tweet?text=${quote} - ${author}`;
window.open(twitterUrl, '_blank');
}
}
// Hook up the new tweet event
if (newQuoteBtn && twitterBtn) {
newQuoteBtn.addEventListener('click', getQuote);
twitterBtn.addEventListener('click', tweetQuote);
}
// OnLoad
getQuote();
That's all, we have added typescript to our little quote generator app. Your whole app.ts
should look like this:
const quoteContainer = document.getElementById('quote-container');
const loader = document.getElementById('loader');
const quoteText = document.getElementById('quote');
const authorText = document.getElementById('author');
const twitterBtn = document.getElementById('twitter');
const newQuoteBtn = document.getElementById('new-quote');
interface QuoteData {
quoteAuthor: string;
quoteText: string;
quoteLink?: string;
senderLink?: string;
senderName?: string;
}
const showContentLoader = () => {
if (loader && quoteContainer) {
loader.hidden = false;
quoteContainer.hidden = true;
}
}
const hideContentLoader = () => {
if (loader && quoteContainer) {
if (!loader.hidden) {
quoteContainer.hidden = false;
loader.hidden = true;
}
}
}
// Get quote from API
const getQuote = async () => {
showContentLoader();
const proxyUrl = 'https://cors-anywhere.herokuapp.com/'
const apiUrl = `https://api.forismatic.com/api/1.0/?method=getQuote&lang=en&format=json`;
try {
const response = await fetch(proxyUrl + apiUrl);
const data: QuoteData = await response.json();
if (authorText && quoteText) {
// default to annoynmous if there is no author
data.quoteAuthor === ''
? authorText.innerText = 'Anoynmous'
: authorText.innerText = data.quoteAuthor;
// Dynamically change text size
data.quoteText.length > 120
? quoteText.classList.add('long-quote')
: quoteText.classList.remove('long-quote');
quoteText.innerText = data.quoteText;
// show quote
hideContentLoader();
}
} catch (error) {
getQuote();
}
}
// Tweet quote
const tweetQuote = () => {
if (quoteText && authorText) {
const quote = quoteText.innerText;
const author = authorText.innerText;
const twitterUrl = `https://twitter.com/intent/tweet?text=${quote} - ${author}`;
window.open(twitterUrl, '_blank');
}
}
// Hook up the new tweet event
if (newQuoteBtn && twitterBtn) {
newQuoteBtn.addEventListener('click', getQuote);
twitterBtn.addEventListener('click', tweetQuote);
}
// OnLoad
getQuote();
Final Step
To get your new typescript file ready for the browser, open the terminal and run the build command again.
tsc --build tsconfig.json
Todo
You can optimize the getQuote
function, it's recursive nature could mean a perpetual loading or crashing of our app if anything happens with the API providers. Set up a mechanism to guard against that. See the GitHub code here
See you in the next #JStoTSConversion
.