I just ran into the following situation in Vue3 and prayed at the altar of the TypeScript gods for an answer. After much beard-pulling and some choice phrases, I found something that works. Is this the way? I cannot confirm nor deny that 😃 but it works and makes sense as well.
Problem
<td v-for="value in currentRecord" :key="recordId">
{{ value.displayText ? value.displayText : value }}
</td>
Seems innocent enough right? Well, TypeScript had something to say about that, quote: 'value' is of type 'unknown'. ts(18046)
. My first thought was to type value
but there seems to be no way to do this inside the template portion of a single file component.
So where does currentRecord
come from? Over here:
const currentRecord = Object.fromEntries(
Object.entries(props.record).filter(([key]) => key !== "id")
);
The Solution
If I typed currentRecord
, would TypeScript then be happy inside the template? Yes it will, but there is another problem here. The currentRecord
object is a bit tricky in that it can contain properties that have a number, or string as the value and some can be an object itself. Something like this:
{
id: 1,
word: "Monkey",
phoneme: "i",
blendDirection: {
id: 1,
displayText: "reverse",
},
}
I could create a type that details every property of the object and where relevant indicates that it can be a string or an object like this:
type Record = {
id: number;
word: string;
blendDirection: string | {
id: number;
displayText: string;
};
// other properties
};
However, there is another way that will be a bit less of a maintenance burden and is then also the type I used in the end:
type Record = {
[key: string]:
| string
| number
| undefined
| {
id: number;
displayText: string;
};
};
With this, we have an object that contains a key
of type string
that can have any of the listed values. Because undefined
is one of those, the property itself can be optional. Now let's type currentRecord
. Doing it this way will make TypeScript sad:
const currentRecord: Record = Object.fromEntries(
Object.entries(props.record).filter(([key]) => key !== "id")
);
What you want to do is this:
const currentRecord = Object.fromEntries(
Object.entries(props.record).filter(([key]) => key !== "id")
) as Record;
OK, now everything should be good, right?
Wrongo!
The first thing that TypeScript is going to complain about is that value
can be undefined. I get why it would say that we told TypeScript that that could be the case. So this should do the trick:
{{ value?.displayText ? value.displayText : value }}
Big Nope!
Because the value could be a string
or a number
, TypeScript will say, Property 'displayText' does not exist on type 'string | number | { id: number; displayText: string; }'.
Property 'displayText' does not exist on type 'string'.ts(2339)
Fair enough 🥺 - So what to do... The final piece of the puzzle is to check whether value
is an object
and only then grab displayText
, like so:
{{ typeof value === 'object' ? value.displayText : value }}
And that is it! Now, your code works (I did already, but we want to ensure TypeScript is happy as well right?) and TypeScript is happy.
I hope you found this helpful and that it will save you some time and hair. Happy coding!