I have been having endless trouble with a typescript error in one location in my code that is preventing me from having successful builds. I even asked on stack overflow and didn't get any answers. Here is the full repo: https://github.com/markdrecoll/word_game/blob/main/src/hooks/useWordGame.tsx This is a wordle clone that I followed a guide on but it was written in javascript not typescript.
I managed to get the errors down to just three locations (line 69 and below) at the prevUsedKeys[l.key].
If I set usedKeys initially to [] the error is only in the [] of prevUsedKeys : Element implicitly has an 'any' type because index expression is not of type 'number'
If I set usedKeys initially to {} the error is on the whole prevUsedKeys[] and is: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type '{}'
If anyone could help with this I would be extremely grateful.
import { useState } from 'react';
import WORD_LIST from "../constants/word_list.json"
interface LetterInfo {
key: string,
color: string,
}
const useWordGame = (secretWord: string | null[]) => {
const [turn, setTurn] = useState(0);
const [currentGuess, setCurrentGuess] = useState("");
const [guesses, setGuesses] = useState([...Array(6)]); // each guess is an array
const [history, setHistory] = useState([""]); // each guess is a string
const [isCorrect, setIsCorrect] = useState(false);
const [usedKeys, setUsedKeys] = useState({});
const [notificationText, setNotificationText] = useState("");
const [showNotification, setShowNotification] = useState(false);
// format a guess into an array of letter objects
// e.g. [{key: 'a', color: 'yellow'}]
const formatGuess = () => {
let secretWordArray = [...secretWord];
let formattedGuess = [...currentGuess].map((l) => {
return { key: l, color: 'grey' };
});
// find any green letters
formattedGuess.forEach((l, i) => {
if (secretWord[i] === l.key) {
formattedGuess[i].color = 'green';
secretWordArray[i] = null;
}
});
// find any yellow letters
formattedGuess.forEach((l, i) => {
if (secretWordArray.includes(l.key) && l.color !== 'green') {
formattedGuess[i].color = 'yellow';
secretWordArray[secretWordArray.indexOf(l.key)] = null;
}
});
return formattedGuess;
}
// add a new guess to the guesses state
// update the isCorrect state if the guess is correct
// add one to the turn state
const addNewGuess = (formattedGuess: Array<LetterInfo>) => {
if (currentGuess === secretWord) {
setIsCorrect(true);
}
setGuesses(prevGuesses => {
let newGuesses = [...prevGuesses];
newGuesses[turn] = formattedGuess;
return newGuesses;
})
setHistory(prevHistory => {
return [...prevHistory, currentGuess];
})
setTurn(prevTurn => {
return prevTurn + 1;
})
setUsedKeys(prevUsedKeys => {
formattedGuess.map(l => {
const currentColor: any = prevUsedKeys[l.key as keyof typeof prevUsedKeys];
if (l.color === 'green') {
prevUsedKeys[l.key] = 'green';
return;
}
if (l.color === 'yellow' && currentColor !== 'green') {
prevUsedKeys[l.key] = 'yellow';
return;
}
if (l.color === 'grey' && currentColor !== ('green' || 'yellow')) {
prevUsedKeys[l.key] = 'grey';
return;
}
})
return prevUsedKeys;
})
setCurrentGuess("");
}
// handle keyup event & track current guess
// if user presses enter, add the new guess
const handleKeyup = ({ key }: KeyboardEvent) => {
let matchFound: Boolean = false;
if (key === 'Enter') {
// guess must be a word
for (let i = 0; i < WORD_LIST.length; i++) {
if (WORD_LIST[i].toUpperCase() === currentGuess) {
matchFound = true;
}
}
if (matchFound !== true){
setNotificationText("That is not a word.");
setShowNotification(true);
return;
}
// only add guess if turn is less than 5
if (turn > 5) {
return;
}
// do not allow duplicate words
if (history.includes(currentGuess)) {
setNotificationText("You already tried that word.");
setShowNotification(true);
return;
}
// check if word is 5 characters
// if (currentGuess.length !== 5) {
// setNotificationText("Word must be 5 letters long.");
// setShowNotification(true);
// return;
// }
console.log("showNotification state in hook", showNotification);
const formatted: Array<LetterInfo> = formatGuess();
addNewGuess(formatted);
}
if (key === 'Backspace') {
setCurrentGuess(prev => prev.slice(0, -1));
return;
}
if (/^[A-Za-z]$/.test(key)) {
if (currentGuess.length < 5) {
setCurrentGuess(prev => prev + key.toUpperCase());
}
}
}
// Reset the game board if the user wants to play another game.
const handleNewGame = () => {
setTurn(0);
setCurrentGuess("");
setGuesses([...Array(6)]) // each guess is an array
setHistory([]) // each guess is a string
setIsCorrect(false);
setUsedKeys({});
setNotificationText("");
}
return {
turn,
currentGuess,
guesses,
isCorrect,
usedKeys,
handleKeyup,
handleNewGame,
notificationText,
showNotification
}
}
export default useWordGame;
The underlying issue is that Typescript, although good at inference, is hardly perfect and can't determine a statement such as {}
or []
(from its point of view, it would either be considered a Record
or Array
of either any
/unknown
depending on the typescript version you're running). A way around this is to provide the type you're trying to match. This can be done by doing: useState<TYPE | INTERFACE>()
.
Based on the code you provided, it looks like usedKeys
is an object that accepts a string as both the key as well as the value. We could then do something like:
const [usedKeys, setUsedKeys] = useState<Record<string, string>>({});
this lets the typescript compiler (tsc) know exactly what to expect.