In this task, we’ll refactor the existing JavaScript code to TypeScript. TypeScript is a superset of JavaScript that adds static type checking and other features to help you write more robust and maintainable code.
We can start by declaring types for the API response data. Currently, the API response is an array of objects with the following structure:
[
{
word: "hello",
phonetic: "hə'ləʊ",
phonetics: [
{
text: "hə'ləʊ",
audio:
"//ssl.gstatic.com/dictionary/static/sounds/20200429/hello--_gb_1.mp3",
},
{
text: "hɛ'ləʊ",
},
],
origin: "early 19th century: variant of earlier hollo ; related to holla.",
meanings: [
{
partOfSpeech: "exclamation",
definitions: [
{
definition: "used as a greeting or to begin a phone conversation.",
example: "hello there, Katie!",
synonyms: [],
antonyms: [],
},
],
},
{
partOfSpeech: "noun",
definitions: [
{
definition: "an utterance of 'hello'; a greeting.",
example: "she was getting polite nods and hellos from people",
synonyms: [],
antonyms: [],
},
],
},
{
partOfSpeech: "verb",
definitions: [
{
definition: "say or shout 'hello'.",
example: "I pressed the phone button and helloed",
synonyms: [],
antonyms: [],
},
],
},
],
},
]
The above is the example response provided on the Free Dictionary API homepage. We can create type definitions for the API response data.
Definition
ObjectLet’s start by declaring a type for the Definition
object:
type Definition = {
definition: string;
example?: string;
synonyms?: string[];
antonyms?: string[];
};
The Definition
type represents the structure of the Definition
object in the API response. It has four properties:
definition
: A string representing the definition of the word.example
: An string representing an example sentence using the word. In our exploration of the API response, we found that this property may be missing in some cases. Therefore, we’ve marked it as optional using the ?
operator.synonyms
: An array of strings representing synonyms of the word. Similar to the example
property, this property may be missing in some cases, so we’ve marked it as optional.antonyms
: An array of strings representing antonyms of the word. This property is also optional since it may be missing in some cases.Notice we use the keyword type
to define a new type in TypeScript. The general syntax for defining a type is type TypeName = { /* type definition */ };
. Moreover, we define the properties of the type using the colon (:
) syntax, where we specify the property name followed by the property type. For example, definition: string
specifies that the definition
property is of type string
.
In TypeScript, the following basic types cover most of the use cases:
string
: Represents a string value.number
: Represents a numeric value.boolean
: Represents a boolean value (true
or false
).object
: Represents a JavaScript object.To declare an array type, we use the syntax Type[]
, where Type
is the type of the elements in the array. For example, string[]
represents an array of strings.
We often use these basic types to annotate the data type of variables, function parameters, and return values in TypeScript. For example, consider the following variable declaration:
const dictionaryAPI = "https://api.dictionaryapi.dev/api/v2/entries/en_US/";
In TypeScript, we can annotate the type of the dictionaryAPI
variable as follows:
const dictionaryAPI: string = "https://api.dictionaryapi.dev/api/v2/entries/en_US/";
Notice how the type annotation : string
is added after the variable name to specify that the dictionaryAPI
variable is of type string
. This is followed by the assignment operator (=
) and the value of the variable.
The type annotations are optional in TypeScript, as TypeScript can infer the types from the assigned values. However, adding type annotations can help catch errors early and improve code readability.
The real power of TypeScript comes from defining custom types and interfaces to represent complex data structures. Let’s continue defining types for the API response data.
Meaning
ObjectNext, let’s declare a type for the Meaning
object:
type Meaning = {
partOfSpeech: string;
definitions: Definition[];
};
The Meaning
type represents the structure of the Meaning
object in the API response. It has two properties:
partOfSpeech
: A string representing the part of speech of the word.definitions
: An array of Definition
objects representing the definitions of the word.Notice that the definitions
property is an array of Definition
objects. This is an example of how we can compose types in TypeScript. We can use one type as a property of another type, creating a more complex structure.
Phonetic
ObjectNext, let’s declare a type for the Phonetic
object:
type Phonetic = {
text?: string;
audio?: string;
};
The Phonetic
type represents the structure of the Phonetic
object in the API response. It has two properties:
text
: A string representing the phonetic text. We found that this property may be missing in some cases, so we’ve marked it as optional.audio
: A string representing the URL to the audio file for the phonetic text. This property is also optional since it may be missing in some cases.DictionaryAPIResponse
ObjectFinally, let’s declare a type for the DictionaryAPIResponse
object, which represents the structure of the API response data:
type DictionaryAPIResponse = {
word: string;
phonetic?: string;
phonetics: Phonetic[];
origin?: string;
meanings: Meaning[];
};
The DictionaryAPIResponse
type represents the structure of the API response data. It has five properties:
word
: A string representing the word which the response data is about.phonetic
: A string representing the phonetic pronunciation of the word. This property is marked optional since it may be missing in some cases.phonetics
: An array of Phonetic
objects representing the phonetic information of the word.origin
: A string representing the origin of the word. This property is optional.meanings
: An array of Meaning
objects representing the meanings of the word.With these type definitions in place, we can now use them to annotate the API response data in our code.
In TypeScript, we can also use interfaces to define object types. Interfaces are similar to type aliases but have some differences in how they can be extended and implemented. For most use cases, type aliases and interfaces are interchangeable, so you can choose the one that fits your coding style.
I prefer using interfaces for defining object types because I can use the extends
keyword to create subtypes and extend existing types. This is a familiar concept if you’ve worked with object-oriented programming languages. On the other hand, type aliases are more like sets of types that can be combined using union (|
) or intersection (&
) operators.
Let’s rewrite the type definitions we created earlier using interfaces:
interface Definition {
definition: string;
example?: string;
synonyms?: string[];
antonyms?: string[];
}
interface Meaning {
partOfSpeech: string;
definitions: Definition[];
}
interface Phonetic {
text?: string;
audio?: string;
}
interface DictionaryAPIResponse {
word: string;
phonetic?: string;
phonetics: Phonetic[];
origin?: string;
meanings: Meaning[];
}
Notice the syntax for defining interfaces: interface InterfaceName { /* interface definition */ };
. The properties of the interface are defined in the same way as type aliases, using the colon (:
) syntax to specify the property name followed by the property type.
As far as the syntaxt goes, the difference between type aliases and interfaces is minimal:
type
keyword, while for interfaces, we use the interface
keyword.=
to assign a type to a type alias and { /* type definition */ }
to define the type structure. For interfaces, we use { /* interface definition */ }
to define the interface structure. (The =
sign is not used with interfaces.)In the next steps, we’ll every function parameter and return value with the appropriate type annotations.
searchWord
FunctionHere is the original searchWord
function:
// Fetch data from the API
const searchWord = async (word) => {
try {
const response = await fetch(`${dictionaryAPI}${word}`);
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching data: ", error);
}
};
We can refactor this function to include type annotations for the word
parameter and the return value. Here’s the refactored version using type annotations:
// Fetch data from the API
const searchWord = async (
word: string,
): Promise<DictionaryAPIResponse[] | undefined> => {
try {
const response = await fetch(`${dictionaryAPI}${word}`);
const data: DictionaryAPIResponse[] = await response.json();
return data;
} catch (error) {
console.error("Error fetching data: ", error);
}
};
Let’s break down the changes:
word
parameter: word: string
. This specifies that the word
parameter should be a string.data
variable: const data: DictionaryAPIResponse[]
. This specifies that the data
variable should be an array of DictionaryAPIResponse
objects.Promise<DictionaryAPIResponse[] | undefined>
. This specifies that the function returns a promise that resolves to an array of DictionaryAPIResponse
objects or undefined
.Let’s focus on the syntax of the return type annotation: Promise<DictionaryAPIResponse[] | undefined>
. This indicates that the searchWord
function returns a promise. The angle brackets (< >
) contain the type of the resolved value of the promise. If you have worked with generics in other languages, like Java, this syntax might look familiar. In this case, they type of the resolved value is DictionaryAPIResponse[] | undefined
. The pipe (|
) operator is used to specify that the resolved value can be an array of DictionaryAPIResponse
objects or undefined
.
The undefined
type is used to represent a value that may be undefined
. In TypeScript, undefined
is a valid value that can be assigned to variables. By including undefined
in the return type, we indicate that the function may return undefined
if an error occurs during the API call. The catch
block in the function handles the error case, where we log an error message to the console. The missing return
statement in the catch
block implicitly returns undefined
.
You might be wondering why we use Promise<DictionaryAPIResponse[] | undefined>
instead of just DictionaryAPIResponse[] | undefined
. The reason is that the searchWord
function is asynchronous. Therefore, the return type of the searchWord
function should reflect this asynchronous nature by using Promise
.
Aside: The fetch
throws error due to network issues, not due to the response status code. So, if the network request fails, the catch
block will be executed. However, if the request is unsuccessful due to any issues, the API server will still return a response with a status code of 4xx or 5xx. In such cases, the response.ok
property will be false
. We can check this property to handle errors due to response status codes.
// Fetch data from the API
const searchWord = async (
word: string,
): Promise<DictionaryAPIResponse[] | undefined> => {
try {
const response = await fetch(`${dictionaryAPI}${word}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: DictionaryAPIResponse[] = await response.json();
return data;
} catch (error) {
console.error("Error fetching data: ", error);
}
};
In this updated version, we added a check for the response.ok
property to handle errors due to response status codes. If the response.ok
property is false
, we throw an error with the HTTP status code. This allows us to handle failed requests similar to network errors.
extractWordDefinitions
FunctionHere is the original extractWordDefinitions
function:
// Extract word definitions from the data
const extractWordDefinitions = (data) => {
if (data && Array.isArray(data)) {
if (data[0].meanings && Array.isArray(data[0].meanings)) {
return data[0].meanings;
}
}
};
We can refactor this function to include type annotations for the data
parameter and the return value. Here’s the refactored version using type annotations:
// Extract word definitions from the data
const extractWordDefinitions = (
data: DictionaryAPIResponse[] | undefined,
): Meaning[] | undefined => {
return data?.[0]?.meanings ?? undefined;
};
Let’s break down the changes:
We added a type annotation for the data
parameter: data: DictionaryAPIResponse[] | undefined
. This specifies that the data
parameter should be an array of DictionaryAPIResponse
objects or undefined
.
We added a return type annotation for the function: Meaning[] | undefined
. This specifies that the function returns an array of Meaning
objects or undefined
.
We used optional chaining (?.
) and the nullish coalescing operator (??
) to safely access nested properties and handle cases where the data structure may be incomplete or missing.
?.
)The optional chaining operator (?.
) allows you to safely access nested properties of an object without causing an error if a property is null
or undefined
. If the property you are trying to access is null
or undefined
, the expression short-circuits and returns undefined
. This can help prevent TypeError
errors when working with nested data structures.
??
)The nullish coalescing operator (??
) provides a way to provide a default value when a value is null
or undefined
. If the value on the left side of the operator is null
or undefined
, the operator returns the value on the right side. Otherwise, it returns the value on the left side. This can be useful for providing fallback values when dealing with potentially null
or undefined
values.
The ??
is a shorter way of writing the following conditional expression:
const value = someValue !== null && someValue !== undefined ? someValue : defaultValue;
By using the nullish coalescing operator, we can write more concise and readable code when handling default values.
The combination of optional chaining and the nullish coalescing operator allowed us to refactor the following code into a single line:
if (data && Array.isArray(data)) {
if (data[0].meanings && Array.isArray(data[0].meanings)) {
return data[0].meanings;
}
}
extractWordPhonetics
FunctionHere is the original extractWordPhonetics
function:
// Extract word phonetics from the data
const extractWordPhonetics = (data) => {
if (data && Array.isArray(data)) {
if (data[0].phonetics && Array.isArray(data[0].phonetics)) {
return data[0].phonetics;
}
}
};
We can refactor this function to include type annotations for the data
parameter and the return value. Here’s the refactored version using type annotations:
// Extract word phonetics from the data
const extractWordPhonetics = (
data: DictionaryAPIResponse[] | undefined,
): Phonetic[] | undefined => {
return data?.[0]?.phonetics ?? undefined;
};
Similar to the extractWordDefinitions
function, we used optional chaining and the nullish coalescing operator to safely access nested properties and handle cases where the data structure may be incomplete or missing.
clearDefinitionsSection
FunctionHere is the original clearDefinitionsSection
function:
// Helper to clear the definitions section
const clearDefinitionsSection = () => {
const definitionsSection = document.getElementById("definitions");
definitionsSection.innerHTML = "";
return definitionsSection;
};
We can refactor this function to include type annotations for the return value. Here’s the refactored version using type annotations:
// Helper to clear the definitions section
const clearDefinitionsSection = (): HTMLElement => {
const definitionsSection = document.getElementById("definitions") as HTMLElement;
definitionsSection.innerHTML = "";
return definitionsSection;
};
We added a return type annotation for the function: HTMLElement
. This specifies that the function returns an HTMLElement
object. Notice that we used a type assertion (as HTMLElement
) to cast the return value of document.getElementById("definitions")
to an HTMLElement
. This is necessary because the getElementById
method returns an Element | null
type, and we need to specify that the return value is an HTMLElement
.
Type assertion in TypeScript is a way to tell the compiler about the type of a variable when the compiler cannot infer it automatically. It is like type casting in other languages, but it does not change the type of the variable at runtime. Instead, it is used by the TypeScript compiler to understand the type of a variable during static analysis.
createDefinitionsHeading
FunctionHere is the original createDefinitionsHeading
function:
// Helper to create the definitions heading
const createDefinitionsHeading = () => {
const definitionsHeading = document.createElement("h1");
definitionsHeading.classList.add("text-2xl", "font-semibold");
definitionsHeading.innerText = "Definitions";
return definitionsHeading;
};
We can refactor this function to include type annotations for the return value. Here’s the refactored version using type annotations:
// Helper to create the definitions heading
const createDefinitionsHeading = (): HTMLElement => {
const definitionsHeading: HTMLElement = document.createElement("h1");
definitionsHeading.classList.add("text-2xl", "font-semibold");
definitionsHeading.innerText = "Definitions";
return definitionsHeading;
};
createDefinitionDiv
FunctionHere is the original createDefinitionDiv
function:
// Helper to create the definition div
const createDefinitionDiv = () => {
const definitionDiv = document.createElement("div");
definitionDiv.classList.add("bg-sky-50");
return definitionDiv;
};
We can refactor this function to include type annotations for the return value. Here’s the refactored version using type annotations:
// Helper to create the definition div
const createDefinitionDiv: HTMLElement = (): HTMLElement => {
const definitionDiv = document.createElement("div");
definitionDiv.classList.add("bg-sky-50");
return definitionDiv;
};
createPartOfSpeechElement
FunctionHere is the original createPartOfSpeechElement
function:
// Helper to create the part of speech element
const createPartOfSpeechElement = (partOfSpeech) => {
const partOfSpeechName = document.createElement("p");
partOfSpeechName.classList.add(
"px-4",
"py-2",
"font-semibold",
"text-white",
"bg-sky-600",
);
partOfSpeechName.innerText = partOfSpeech;
return partOfSpeechName;
};
We can refactor this function to include type annotations for the partOfSpeech
parameter and the return value. Here’s the refactored version using type annotations:
// Helper to create the part of speech element
const createPartOfSpeechElement = (partOfSpeech: string): HTMLElement => {
const partOfSpeechName: HTMLElement = document.createElement("p");
partOfSpeechName.classList.add(
"px-4",
"py-2",
"font-semibold",
"text-white",
"bg-sky-600",
);
partOfSpeechName.innerText = partOfSpeech;
return partOfSpeechName;
};
createDefinitionsList
FunctionHere is the original createDefinitionsList
function:
// Helper to create the definitions list
const createDefinitionsList = () => {
const definitionsList = document.createElement("ul");
definitionsList.classList.add(
"p-2",
"ml-6",
"font-light",
"list-disc",
"text-sky-700",
);
return definitionsList;
};
We can refactor this function to include type annotations for the return value. Here’s the refactored version using type annotations:
// Helper to create the definitions list
const createDefinitionsList = (): HTMLElement => {
const definitionsList: HTMLElement = document.createElement("ul");
definitionsList.classList.add(
"p-2",
"ml-6",
"font-light",
"list-disc",
"text-sky-700",
);
return definitionsList;
};
createDefinitionItem
FunctionHere is the original createDefinitionItem
function:
// Helper to create the definition item
const createDefinitionItem = (definitionObj) => {
const definitionsItem = document.createElement("li");
definitionsItem.innerText = definitionObj.definition;
return definitionsItem;
};
We can refactor this function to include type annotations for the definitionObj
parameter and the return value. Here’s the refactored version using type annotations:
// Helper to create the definition item
const createDefinitionItem = (definitionObj: Definition): HTMLElement => {
const definitionsItem: HTMLElement = document.createElement("li");
definitionsItem.innerText = definitionObj.definition;
return definitionsItem;
};
Notice we used our custom Definition
type to annotate the definitionObj
parameter. This ensures that the definitionObj
parameter should have the structure defined by the Definition
type. For example, the definitionObj
parameter should have a definition
property of type string
. If the definitionObj
parameter does not match the Definition
type, TypeScript will raise a type error. However, this only happens during static analysis, so you won’t see runtime errors due to type mismatches. So, if we make an API call and the response structure changes, TypeScript will not catch the errors when the application is running.
TypeScript is a statically typed language, which means that type checking is done at compile time. Once the TypeScript code is compiled to JavaScript, the type annotations are removed, and the resulting JavaScript code does not contain any type information. This process is known as type erasure.
When the JavaScript code runs in the browser or Node.js environment, there is no type checking happening at runtime. The type annotations are purely for the benefit of the developer and the TypeScript compiler. This means that any type errors that are not caught during compilation will not be detected at runtime.
displayWordDefinition
FunctionHere is the original displayWordDefinition
function:
// Display the word definitions
const displayWordDefinition = (meanings) => {
const definitionsSection = clearDefinitionsSection();
const definitionsHeading = createDefinitionsHeading();
definitionsSection.appendChild(definitionsHeading);
meanings.forEach((meaning) => {
const definitionDiv = createDefinitionDiv();
definitionsSection.appendChild(definitionDiv);
const { partOfSpeech, definitions } = meaning;
const partOfSpeechName = createPartOfSpeechElement(partOfSpeech);
definitionDiv.appendChild(partOfSpeechName);
const definitionsList = createDefinitionsList();
definitionDiv.appendChild(definitionsList);
const definitionListItems = definitions.map(createDefinitionItem);
definitionsList.append(...definitionListItems);
});
};
We can refactor this function to include type annotations for the meanings
parameter. Here’s the refactored version using type annotations:
// Display the word definitions
const displayWordDefinition = (meanings: Meaning[] | undefined): void => {
const definitionsSection: HTMLElement = clearDefinitionsSection();
const definitionsHeading: HTMLElement = createDefinitionsHeading();
definitionsSection.appendChild(definitionsHeading);
meanings?.forEach((meaning: Meaning) => {
const definitionDiv: HTMLElement = createDefinitionDiv();
definitionsSection.appendChild(definitionDiv);
const { partOfSpeech, definitions } = meaning;
const partOfSpeechName: HTMLElement = createPartOfSpeechElement(partOfSpeech);
definitionDiv.appendChild(partOfSpeechName);
const definitionsList: HTMLElement = createDefinitionsList();
definitionDiv.appendChild(definitionsList);
const definitionListItems: HTMLElement[] = definitions.map(createDefinitionItem);
definitionsList.append(...definitionListItems);
});
};
Notice the return type annotation for the function: void
. This specifies that the function does not return a value. The function is responsible for updating the DOM to display the word definitions but does not return any data.
In TypeScript, the void
type is used to indicate that a function does not return a value. It helps make the intention clear that the function is not meant to return anything, even though in JavaScript, functions that do not explicitly return a value do return undefined
.
Here’s how it works:
undefined
.void
: TypeScript uses the void
type to signify that a function is not supposed to return a value. It’s a way to document the intent and enforce that the function does not return any value that should be used.When you annotate a function with void
in TypeScript, it means you should not return a value from that function. If you try to return a value from a function annotated with void
, TypeScript will give a compile-time error.
Here are the original functions related to phonetics:
// Helper to clear the phonetics section
const createPhoneticsSection = () => {
const phoneticsSection = document.getElementById("phonetics");
phoneticsSection.innerHTML = "";
phoneticsSection.classList.add("flex", "flex-col", "gap-4");
return phoneticsSection;
};
// Helper to create the phonetics heading
const createPhoneticsHeading = () => {
const phoneticsHeading = document.createElement("h1");
phoneticsHeading.classList.add("text-2xl", "font-semibold");
phoneticsHeading.innerText = "Phonetics";
return phoneticsHeading;
};
// Helper to create the phonetics div
const createPhoneticsDiv = () => {
const phoneticsDiv = document.createElement("div");
phoneticsDiv.classList.add("bg-stone-100");
return phoneticsDiv;
};
// Helper to create the phonetic element
const createPhoneticElement = (text) => {
const phoneticText = document.createElement("p");
phoneticText.classList.add("px-4", "py-3", "text-white", "bg-stone-700");
phoneticText.innerText = text;
return phoneticText;
};
// Helper to create the audio control
const createAudioControl = () => {
const audioControl = document.createElement("audio");
audioControl.style = "width: 100%";
audioControl.setAttribute("controls", "true");
return audioControl;
};
// Helper to create the audio source
const createAudioSource = (audio) => {
const source = document.createElement("source");
source.setAttribute("src", audio);
source.setAttribute("type", "audio/mpeg");
return source;
};
// Helper to create the fallback text
const fallbackText = document.createTextNode(
"Your browser does not support the audio element.",
);
// Display the word phonetics
const displayWordPhonetic = (phonetics) => {
const phoneticsSection = createPhoneticsSection();
const phoneticsHeading = createPhoneticsHeading();
phoneticsSection.appendChild(phoneticsHeading);
phonetics.forEach((phonetic) => {
const { text, audio } = phonetic;
if (!text || !audio) return;
const phoneticsDiv = createPhoneticsDiv();
phoneticsSection.appendChild(phoneticsDiv);
const phoneticText = createPhoneticElement(text);
phoneticsDiv.appendChild(phoneticText);
const audioControl = createAudioControl();
phoneticsDiv.appendChild(audioControl);
const source = createAudioSource(audio);
audioControl.appendChild(source);
const fallBackText = fallbackText;
audioControl.appendChild(fallBackText);
});
};
We can refactor these functions to include type annotations for the parameters and return values. Here’s the refactored version using type annotations:
// Helper to clear the phonetics section
const createPhoneticsSection = (): HTMLElement => {
const phoneticsSection: HTMLElement = document.getElementById(
"phonetics",
) as HTMLElement;
phoneticsSection.innerHTML = "";
phoneticsSection.classList.add("flex", "flex-col", "gap-4");
return phoneticsSection;
};
// Helper to create the phonetics heading
const createPhoneticsHeading = (): HTMLElement => {
const phoneticsHeading: HTMLElement = document.createElement("h1");
phoneticsHeading.classList.add("text-2xl", "font-semibold");
phoneticsHeading.innerText = "Phonetics";
return phoneticsHeading;
};
// Helper to create the phonetics div
const createPhoneticsDiv = (): HTMLElement => {
const phoneticsDiv: HTMLElement = document.createElement("div");
phoneticsDiv.classList.add("bg-stone-100");
return phoneticsDiv;
};
// Helper to create the phonetic element
const createPhoneticElement = (text: string): HTMLElement => {
const phoneticText: HTMLElement = document.createElement("p");
phoneticText.classList.add("px-4", "py-3", "text-white", "bg-stone-700");
phoneticText.innerText = text;
return phoneticText;
};
// Helper to create the audio control
const createAudioControl = (): HTMLAudioElement => {
const audioControl: HTMLAudioElement = document.createElement("audio");
audioControl.style.width = "100%";
audioControl.setAttribute("controls", "true");
return audioControl;
};
// Helper to create the audio source
const createAudioSource = (audio: string): HTMLSourceElement => {
const source: HTMLSourceElement = document.createElement("source");
source.setAttribute("src", audio);
source.setAttribute("type", "audio/mpeg");
return source;
};
// Helper to create the fallback text
const fallbackText: Text = document.createTextNode(
"Your browser does not support the audio element.",
);
// Display the word phonetics
const displayWordPhonetic = (phonetics: Phonetic[] | undefined): void => {
const phoneticsSection: HTMLElement = createPhoneticsSection();
const phoneticsHeading: HTMLElement = createPhoneticsHeading();
phoneticsSection.appendChild(phoneticsHeading);
phonetics?.forEach((phonetic: Phonetic) => {
const { text, audio } = phonetic;
if (!text || !audio) return;
const phoneticsDiv: HTMLElement = createPhoneticsDiv();
phoneticsSection.appendChild(phoneticsDiv);
const phoneticText: HTMLElement = createPhoneticElement(text);
phoneticsDiv.appendChild(phoneticText);
const audioControl: HTMLAudioElement = createAudioControl();
phoneticsDiv.appendChild(audioControl);
const source: HTMLSourceElement = createAudioSource(audio);
audioControl.appendChild(source);
const fallBackText: Text = fallbackText;
audioControl.appendChild(fallBackText);
});
};
Here is the original event listener code:
// Get the input word and search for its definition
const inputWord = document.getElementById("input");
const submitBtn = document.getElementById("submit");
submitBtn.addEventListener("click", (event) => {
const word = inputWord.value.trim();
if (!word) return;
searchWord(word)
.then((data) => {
console.log(data);
const meanings = extractWordDefinitions(data);
displayWordDefinition(meanings);
const phonetics = extractWordPhonetics(data);
displayWordPhonetic(phonetics);
})
.catch((error) => {
console.error("Error: ", error);
});
});
We can refactor this event listener code to include type annotations and use the updated functions. Here’s the refactored version using type annotations:
// Get the input word and search for its definition
const inputWord = document.getElementById("input") as HTMLInputElement;
const submitBtn = document.getElementById("submit") as HTMLButtonElement;
submitBtn.addEventListener("click", (event: Event) => {
event.preventDefault();
const word: string = inputWord.value.trim();
if (!word) return;
searchWord(word)
.then((data) => {
const meanings = extractWordDefinitions(data);
displayWordDefinition(meanings);
const phonetics = extractWordPhonetics(data);
displayWordPhonetic(phonetics);
})
.catch((error) => {
console.error("Error: ", error);
});
});
Notice the type annotations for the inputWord
and submitBtn
variables. We used type assertions (as HTMLInputElement
and as HTMLButtonElement
) to cast the return values of document.getElementById
to the appropriate types. This is necessary because the getElementById
method returns an Element | null
type, and we need to specify the exact type of the element.
The event listener callback function now includes a type annotation for the event
parameter: event: Event
. This specifies that the event
parameter should be an Event
object. We added a call to event.preventDefault()
to prevent the default form submission behavior when the submit button is clicked. This iss not necessary because the submit button was not part of a form element. However, it’s a good practice to include this line to prevent unexpected behavior in case the button is later added to a form. Moreover, if we don’t use the event
parameter in the callback function, the type-checker will raise a warning that “‘event’ is declared but its value is never read.” To prevent this warning, we can either use the event
parameter as we did here or use an underscore (_
) to indicate that the parameter is intentionally unused:
submitBtn.addEventListener("click", (_event: Event) => {
// Event listener logic
});
We can refactor the event listener callback function to an async function to use the await
keyword with the searchWord
function. Here’s the updated event listener code:
// Get the input word and search for its definition
const inputWord = document.getElementById("input") as HTMLInputElement;
const submitBtn = document.getElementById("submit") as HTMLButtonElement;
submitBtn.addEventListener("click", async (event: Event) => {
event.preventDefault();
const word: string = inputWord.value.trim();
if (!word) return;
try {
const data = await searchWord(word);
const meanings = extractWordDefinitions(data);
displayWordDefinition(meanings);
const phonetics = extractWordPhonetics(data);
displayWordPhonetic(phonetics);
} catch (error) {
console.error("Error: ", error);
}
});
By using the async
keyword with the event listener callback function, we can use the await
keyword with the searchWord
function to wait for the API response. This makes the code more readable and easier to understand. The try-catch
block allows us to handle errors from the searchWord
function in a more structured way.
searchWord
Function to Throw ErrorsThis is the latest version of the searchWord
function that we refactored earlier:
// Fetch data from the API
const searchWord = async (
word: string,
): Promise<DictionaryAPIResponse[] | undefined> => {
try {
const response = await fetch(`${dictionaryAPI}${word}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: DictionaryAPIResponse[] = await response.json();
return data;
} catch (error) {
console.error("Error fetching data: ", error);
}
};
Notice in the catch
block, we log the error to the console but do not re-throw the error. This means that the searchWord
function will not reject the promise with the error. Instead, it will resolve the promise with undefined
. This behavior is fine for the current implementation, but if you want to propagate the error to the caller, you can re-throw the error in the catch
block:
// Fetch data from the API
const searchWord = async (word: string): Promise<DictionaryAPIResponse[]> => {
try {
const response = await fetch(`${dictionaryAPI}${word}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: DictionaryAPIResponse[] = await response.json();
return data;
} catch (error) {
throw error; // Re-throw the error
}
};
By re-throwing the error in the catch
block, the searchWord
function will reject the promise with the error, allowing the caller to handle the error appropriately. This can be useful if you want to handle errors at a higher level in the application or provide more detailed error messages to the user.
Notice we don’t need the | undefined
in the return type annotation because the function will always return a promise that resolves to an array of DictionaryAPIResponse
objects or rejects with an error.
After refactoring the code with type annotations, you can run the application to ensure that everything works as expected. Open the terminal and navigate to the project directory. Then, run the following command to start the development server:
pnpm dev
This command will start the development server and open the application in your default web browser. You can enter a word in the input field and click the “Submit” button to search for its definitions and phonetics. The application should display the word definitions and phonetics on the screen.
To ensure there are no type errors, run the following command:
pnpm type-check
Here is the final version of the refactored code:
import "../style.css";
const dictionaryAPI = "https://api.dictionaryapi.dev/api/v2/entries/en_US/";
interface Definition {
definition: string;
example?: string;
synonyms?: string[];
antonyms?: string[];
}
interface Meaning {
partOfSpeech: string;
definitions: Definition[];
}
interface Phonetic {
text?: string;
audio?: string;
}
interface DictionaryAPIResponse {
word: string;
phonetic?: string;
phonetics: Phonetic[];
origin?: string;
meanings: Meaning[];
}
// Fetch data from the API
const searchWord = async (word: string): Promise<DictionaryAPIResponse[]> => {
try {
const response = await fetch(`${dictionaryAPI}${word}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: DictionaryAPIResponse[] = await response.json();
return data;
} catch (error) {
throw error; // Re-throw the error
}
};
// Extract word definitions from the data
const extractWordDefinitions = (
data: DictionaryAPIResponse[] | undefined,
): Meaning[] | undefined => {
return data?.[0]?.meanings ?? undefined;
};
// Extract word phonetics from the data
const extractWordPhonetics = (
data: DictionaryAPIResponse[] | undefined,
): Phonetic[] | undefined => {
return data?.[0]?.phonetics ?? undefined;
};
// Helper to clear the definitions section
const clearDefinitionsSection = (): HTMLElement => {
const definitionsSection = document.getElementById(
"definitions",
) as HTMLElement;
definitionsSection.innerHTML = "";
return definitionsSection;
};
// Helper to create the definitions heading
const createDefinitionsHeading = (): HTMLElement => {
const definitionsHeading: HTMLElement = document.createElement("h1");
definitionsHeading.classList.add("text-2xl", "font-semibold");
definitionsHeading.innerText = "Definitions";
return definitionsHeading;
};
// Helper to create the definition div
const createDefinitionDiv = (): HTMLElement => {
const definitionDiv: HTMLElement = document.createElement("div");
definitionDiv.classList.add("bg-sky-50");
return definitionDiv;
};
// Helper to create the part of speech element
const createPartOfSpeechElement = (partOfSpeech: string): HTMLElement => {
const partOfSpeechName: HTMLElement = document.createElement("p");
partOfSpeechName.classList.add(
"px-4",
"py-2",
"font-semibold",
"text-white",
"bg-sky-600",
);
partOfSpeechName.innerText = partOfSpeech;
return partOfSpeechName;
};
// Helper to create the definitions list
const createDefinitionsList = (): HTMLElement => {
const definitionsList: HTMLElement = document.createElement("ul");
definitionsList.classList.add(
"p-2",
"ml-6",
"font-light",
"list-disc",
"text-sky-700",
);
return definitionsList;
};
// Helper to create the definition item
const createDefinitionItem = (definitionObj: Definition): HTMLElement => {
const definitionsItem: HTMLElement = document.createElement("li");
definitionsItem.innerText = definitionObj.definition;
return definitionsItem;
};
// Display the word definitions
const displayWordDefinition = (meanings: Meaning[] | undefined): void => {
const definitionsSection: HTMLElement = clearDefinitionsSection();
const definitionsHeading: HTMLElement = createDefinitionsHeading();
definitionsSection.appendChild(definitionsHeading);
meanings?.forEach((meaning: Meaning) => {
const definitionDiv: HTMLElement = createDefinitionDiv();
definitionsSection.appendChild(definitionDiv);
const { partOfSpeech, definitions } = meaning;
const partOfSpeechName: HTMLElement =
createPartOfSpeechElement(partOfSpeech);
definitionDiv.appendChild(partOfSpeechName);
const definitionsList: HTMLElement = createDefinitionsList();
definitionDiv.appendChild(definitionsList);
const definitionListItems: HTMLElement[] =
definitions.map(createDefinitionItem);
definitionsList.append(...definitionListItems);
});
};
// Helper to clear the phonetics section
const createPhoneticsSection = (): HTMLElement => {
const phoneticsSection: HTMLElement = document.getElementById(
"phonetics",
) as HTMLElement;
phoneticsSection.innerHTML = "";
phoneticsSection.classList.add("flex", "flex-col", "gap-4");
return phoneticsSection;
};
// Helper to create the phonetics heading
const createPhoneticsHeading = (): HTMLElement => {
const phoneticsHeading: HTMLElement = document.createElement("h1");
phoneticsHeading.classList.add("text-2xl", "font-semibold");
phoneticsHeading.innerText = "Phonetics";
return phoneticsHeading;
};
// Helper to create the phonetics div
const createPhoneticsDiv = (): HTMLElement => {
const phoneticsDiv: HTMLElement = document.createElement("div");
phoneticsDiv.classList.add("bg-stone-100");
return phoneticsDiv;
};
// Helper to create the phonetic element
const createPhoneticElement = (text: string): HTMLElement => {
const phoneticText: HTMLElement = document.createElement("p");
phoneticText.classList.add("px-4", "py-3", "text-white", "bg-stone-700");
phoneticText.innerText = text;
return phoneticText;
};
// Helper to create the audio control
const createAudioControl = (): HTMLAudioElement => {
const audioControl: HTMLAudioElement = document.createElement("audio");
audioControl.style.width = "100%";
audioControl.setAttribute("controls", "true");
return audioControl;
};
// Helper to create the audio source
const createAudioSource = (audio: string): HTMLSourceElement => {
const source: HTMLSourceElement = document.createElement("source");
source.setAttribute("src", audio);
source.setAttribute("type", "audio/mpeg");
return source;
};
// Helper to create the fallback text
const fallbackText: Text = document.createTextNode(
"Your browser does not support the audio element.",
);
// Display the word phonetics
const displayWordPhonetic = (phonetics: Phonetic[] | undefined): void => {
const phoneticsSection: HTMLElement = createPhoneticsSection();
const phoneticsHeading: HTMLElement = createPhoneticsHeading();
phoneticsSection.appendChild(phoneticsHeading);
phonetics?.forEach((phonetic: Phonetic) => {
const { text, audio } = phonetic;
if (!text || !audio) return;
const phoneticsDiv: HTMLElement = createPhoneticsDiv();
phoneticsSection.appendChild(phoneticsDiv);
const phoneticText: HTMLElement = createPhoneticElement(text);
phoneticsDiv.appendChild(phoneticText);
const audioControl: HTMLAudioElement = createAudioControl();
phoneticsDiv.appendChild(audioControl);
const source: HTMLSourceElement = createAudioSource(audio);
audioControl.appendChild(source);
const fallBackText: Text = fallbackText;
audioControl.appendChild(fallBackText);
});
};
// Get the input word and search for its definition
const inputWord = document.getElementById("input") as HTMLInputElement;
const submitBtn = document.getElementById("submit") as HTMLButtonElement;
submitBtn.addEventListener("click", async (event: Event) => {
event.preventDefault();
const word: string = inputWord.value.trim();
if (!word) return;
try {
const data = await searchWord(word);
const meanings = extractWordDefinitions(data);
displayWordDefinition(meanings);
const phonetics = extractWordPhonetics(data);
displayWordPhonetic(phonetics);
} catch (error) {
console.error("Error: ", error);
}
});
Before concluding this task, I would like to show you how we can make our code more concise and readable by using template strings to create HTML elements. This approach not only simplifies our code but also makes it more familiar to JSX, which we will be using extensively in our upcoming lessons on React. By directly setting the innerHTML
of elements with template strings, we can avoid the verbosity of creating and appending each element individually. Here is the refactored version of our dictionary app that demonstrates this technique:
import "../style.css";
const dictionaryAPI: string =
"https://api.dictionaryapi.dev/api/v2/entries/en_US/";
interface Definition {
definition: string;
example?: string;
synonyms?: string[];
antonyms?: string[];
}
interface Meaning {
partOfSpeech: string;
definitions: Definition[];
}
interface Phonetic {
text?: string;
audio?: string;
}
interface DictionaryAPIResponse {
word: string;
phonetic?: string;
phonetics: Phonetic[];
origin?: string;
meanings: Meaning[];
}
// Fetch data from the API
const searchWord = async (word: string): Promise<DictionaryAPIResponse[]> => {
try {
const response = await fetch(`${dictionaryAPI}${word}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: DictionaryAPIResponse[] = await response.json();
return data;
} catch (error) {
throw error; // Re-throw the error
}
};
// Extract word definitions from the data
const extractWordDefinitions = (
data: DictionaryAPIResponse[] | undefined,
): Meaning[] | undefined => {
return data?.[0]?.meanings ?? undefined;
};
// Extract word phonetics from the data
const extractWordPhonetics = (
data: DictionaryAPIResponse[] | undefined,
): Phonetic[] | undefined => {
return data?.[0]?.phonetics ?? undefined;
};
// Display the word definitions
const displayWordDefinition = (meanings: Meaning[] | undefined): void => {
const definitionsSection = document.getElementById(
"definitions",
) as HTMLElement;
definitionsSection.innerHTML = ""; // Clear previous content
const definitionsHeading = `<h1 class="text-2xl font-semibold">Definitions</h1>`;
definitionsSection.innerHTML += definitionsHeading;
meanings?.forEach((meaning: Meaning) => {
const definitionItems = meaning.definitions
.map((def) => `<li>${def.definition}</li>`)
.join("");
const definitionBlock = `
<div class="bg-sky-50">
<p class="px-4 py-2 font-semibold text-white bg-sky-600">${meaning.partOfSpeech}</p>
<ul class="p-2 ml-6 font-light list-disc text-sky-700">${definitionItems}</ul>
</div>
`;
definitionsSection.innerHTML += definitionBlock;
});
};
// Display the word phonetics
const displayWordPhonetic = (phonetics: Phonetic[] | undefined): void => {
const phoneticsSection = document.getElementById("phonetics") as HTMLElement;
phoneticsSection.innerHTML = ""; // Clear previous content
phoneticsSection.classList.add("flex", "flex-col", "gap-4");
const phoneticsHeading = `<h1 class="text-2xl font-semibold">Phonetics</h1>`;
phoneticsSection.innerHTML += phoneticsHeading;
phonetics?.forEach((phonetic: Phonetic) => {
if (!phonetic.text || !phonetic.audio) return;
const phoneticBlock = `
<div class="bg-stone-100">
<p class="px-4 py-3 text-white bg-stone-700">${phonetic.text}</p>
<audio style="width: 100%" controls>
<source src="${phonetic.audio}" type="audio/mpeg">
Your browser does not support the audio element.
</audio>
</div>
`;
phoneticsSection.innerHTML += phoneticBlock;
});
};
// Get the input word and search for its definition
const inputWord = document.getElementById("input") as HTMLInputElement;
const submitBtn = document.getElementById("submit") as HTMLButtonElement;
submitBtn.addEventListener("click", async (event: Event) => {
event.preventDefault();
const word: string = inputWord.value.trim();
if (!word) return;
try {
const data = await searchWord(word);
const meanings = extractWordDefinitions(data);
displayWordDefinition(meanings);
const phonetics = extractWordPhonetics(data);
displayWordPhonetic(phonetics);
} catch (error) {
console.error("Error: ", error);
}
});
This approach has several benefits:
Conciseness: The code is shorter and easier to read, which can make maintenance simpler.
Familiarity with JSX: Using template strings to construct HTML is similar to JSX, which will help ease the transition to React.
Clarity: It is immediately clear what the HTML structure will be, making the code more intuitive.
However, there are also some drawbacks to consider:
Security: Directly setting innerHTML
can expose your application to cross-site scripting (XSS) attacks if any of the input data is not properly sanitized.
Performance: Modifying the innerHTML
property forces the browser to reparse and reconstruct the DOM, which can be less efficient for large updates compared to manipulating the DOM elements directly.
Maintainability: For complex structures, inline HTML within JavaScript can become harder to manage and debug.
As we move forward into React, you’ll see how JSX provides a more robust solution that combines the benefits of template strings with additional safety and performance optimizations. React’s declarative nature and component-based architecture will help us manage complex UI logic more effectively and securely.