Typescript: declaration vs assertion
If you "need" to use as any
always know it's a code smell. What's more, I think the keyword as
has been used too many times in a code I've seen. I think this is because Typescript developers I have worked with don't distinguish between type assertion and declaration and when they should be using one or another. Spoiler alert, choose to use type declaration over assertion whenever it's possible.
What is a type assertion?
Type assertion happens when you write as XYZ
in Typescript. The most terrifying assertion is as any
The following is still the type assertion:
Whenever you are tempted to use as
remember you are about to tell "Hey, Typescript, I know better than you, here's a hint on how to be a better compiler. Please shut up". In 99% of cases, you are wrong, not Typescript đ
What is a type declaration?
Type declaration is a hint of what you'd like to have. However, instead of forcing Typescript to agree with you, you are asking Typescript to help you! And TS will be pleased to help:
Declaration vs Assertion
Let's think now what are the differences between those 2:
type Member = {
id: number,
firstName: string,
address?: string,
}
const memebr1 = {
firstName: 'Adam',
} as Member
const member2: Member = {
id: 1,
firstName: 'Adam',
}
member1
is not actually aMember
. It lacksid
, a required field forMember
type.- While writing
member2
details I got all Typescript auto-completion working! That was not the case formemebr1
I'm lazy and auto-completion is where I'm sold to declaring types wherever I can. Regarding type safety. You may say you would just make sure to add all the necessary fields. Always think about potential failure in the future. Today Member
has 3 fields only. What happens when you add email: string
"tomorrow"? With type assertions, you wouldn't get any compile time error. With type declaration, Typescript would tell you where you need to add email
here and there.
Type declaration in arrow functions
Using type declaration is easy when it comes to constants/variables. However, devs could have issues with declaring a type with arrow functions. For example in map
:
const names = ['Adam', 'Daniel', 'Jacob']
const members = names.map((name, index) => {
let member: Member = {
id: index,
firstName: name,
}
return member;
})
That doesn't look good. Very often it's changed to:
const names = ['Adam', 'Daniel', 'Jacob']
const members = names.map((name, index) => ({
id: index,
firstName: name,
} as Member))
It works, however, there's no real type-safety and we lack TS hints & auto-completion. There's another way you could declare a type:
const members = names.map((name, index): Member => ({
id: index,
firstName: name,
}))
Type declaration in tests?
Using type assertion in tests is much easier to use as you don't need to fill in all the necessary data (that is not important from a test point of view). I still prefer to use type declaration over assertion just to get all that auto-completion TS gives. I have 2 possible solutions how to make life easier with type declaration in tests:
- Declare a variable as
Partial<XYZ>
orPartialDeep<XYZ>
. You will get all the auto-completion support. When it's time to pass that data into a function that requires non-partial data, use type assertion herefoo(partial as YOUR_ORIGINAL_TYPE)
- Use fixtures! When I'm writing tests I always create a helper like
export const inviteFixture = (
partial?: Partial<AppointmentInvite>,
): AppointmentInvite => {
const defaultInvite: AppointmentInvite = {
id: '321',
appointment_id: '123',
dismissed: false,
expired: false,
duration_minutes: 10,
appointment_reason: 'The invite integration tests',
};
return {
...merge(invite, partial),
};
};
Thanks to the fixture I can create an object inside a test case and just override those values the test case is about: inviteFixture({ id: 'my-new-id', dismissed: true })
.
With the fixture
you increase the readability of the test (a reader knows exactly what properties are important for that test case) and you get the auto-completionď¸. How could you not love fixtures â¤ď¸?