My TypeScript Skills Improving and Best Practices
A Lesson in Debugging and Type Safety
This morning, a seemingly simple issue in my TypeScript project led me down a two-hour debugging rabbit hole. The task at hand was developing a REST service, but it was failing to return all fields of a database record - one field was consistently missing.
The heart of the matter lay in my use of Prisma, a robust database toolkit, for schema definition. Here’s a glimpse of the schema for a better understanding:
model Speaker {
id Int @id @default(autoincrement())
firstName String
lastName String
company String
twitterHandle String
userBioShort String
timeSpeaking DateTime
sessions SpeakerSession[]
favorites AttendeeFavorite[]
}
The problem manifested in the following code segment from my REST service:
async function getSpeakerDataById(id: number) {
const speakerData : Speaker = await prisma.speaker.findUnique({
where: { id },
select: {
id: true,
firstName: true,
lastName: true,
company: true,
twitterHandle: true,
userBioShort: true,
timeSpeaking: true,
_count: {
select: {
favorites: true,
},
},
},
}) ?? {} as Speaker;
Confused, I turned to ChatGPT for an explanation, particularly about the use of {}
and type assertions in TypeScript. The explanation revealed a crucial aspect of TypeScript’s type system:
In TypeScript,
as Speaker
is a type assertion, used to tell TypeScript to treat the object on the left side ofas
as an instance of the Speaker type. Using{}
asSpeaker
means ifprisma.speaker.findUnique
returns null or undefined, TypeScript should treat the empty object{}
as a Speaker object. This can be risky if the Speaker type has mandatory fields, as accessing any property on this empty object would yield undefined, leading to runtime errors.
Realizing the potential pitfalls of my approach, I restructured the code. I first assigned the result to a temporary variable (speakerData
), and then, if not found, threw an error. If found, it was then assigned to a SpeakerRec
.
Here is the adjusted code:
async function getSpeakerDataById(id: number) {
const speakerData : Speaker | null = await prisma.speaker.findUnique({
where: { id },
select: {
id: true,
firstName: true,
lastName: true,
company: true,
twitterHandle: true,
userBioShort: true,
timeSpeaking: true,
_count: {
select: {
favorites: true,
},
},
},
});
if (!speakerData) {
throw new Error("Speaker not found:" + id);
}
const speakerOri : Speaker = speakerData as Speaker;
This adjustment was crucial. Now, if a required field was omitted from the speakerData
declaration, TypeScript would issue a warning. This is because TypeScript’s type system enforces strict adherence to defined types. By explicitly assigning the result to a Speaker
type variable, any mismatch or missing properties in the object structure would be flagged by the compiler, ensuring type safety and reducing the risk of runtime errors.
In summary, this debugging experience was a valuable lesson in TypeScript’s type system and best practices. It highlighted the importance of understanding and correctly utilizing type assertions and error handling to write more reliable and robust code.