Prevent ID switching mistake
Note: Basic understanding of generics and intersection types are assumed in this piece.
Problem
Ever faced a bug that’s caused by feeding in the wrong id to a function? For instance, a function expects studentId
, but you pass in courseId,
given that both ids are of type number
, the function’s signature isn’t violated, and by extension, the compiler allows this.
This issue is particularly pertinent in functions that accept multiple ids
, but the developer mistakenly switches the order of these ids
. Here’s an example:
function recordStudentScore(studentId: number, courseId: number, score: number) {
// do something
}
const studentId: number = 1;
const courseId: number = 2;
const score: number = 3;
// courseId and studentId are switched
recordStudentScore(courseId, studentId, score); //✅︎
Notice that courseId
and studentId
have been switched. Although this code is logically incorrect, it is syntactically valid Typescript code.
What if there was a way to inform the compiler that the first parameter of recordStudentScore
function should accept studentIds
only. By so doing, the cognitive load of tracking the specific order of arguments can be offloaded to the compiler.
Solution
We can create a generic BrandedId
type as shown below. This type is an intersection of 2 types; the first is number
, while the second is a type that has a string name
property. Then, we may create branded types for the different ids we are interested in:
type BrandedId<Name extends string> = number & {
name: Name;
}
type StudentId = BrandedId<"student_id">;
type CourseId = BrandedId<"course_id">;
type Score = BrandedId<"score">;
Wherever a student id is expected, instead of using number
as the param type in the function signature, type StudentId
will be used instead. Same goes for CourseId
and Score
.
Now, let’s rewrite the initial code snippet in an even more type safe manner.
function recordStudentScore(
studentId: StudentId, // StudentId instead of number
courseId: CourseId, // CourseId instead of number
score: Score // CourseId instead of number
) {
// do something
}
const studentId = 1 as StudentId;
const courseId = 2 as CourseId;
const score = 3 as Score;
// courseId and studentId are switched
recordStudentScore(courseId, studentId, score); //❌
// courseId and studentId are in the right order
recordStudentScore(studentId, courseId, score); //✅︎
Due to this name constraint added to the ids, it’s no longer possible to switch them without having compilation errors. As seen above, if courseId
and studentId
are switched, compilation fails.
If you like this content, kindly subscribe to this publication as I will be sharing many more neat tricks. Also here’s my Twitter, if you’d like to reach out.