Typescript: declaration vs assertion

Typescript: declaration vs assertion
Photo by regularguy.eth / Unsplash

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:

const member = { 
  firstName: 'Adam',
  lastName: 'Borek',
} as Member

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:

const member: Member = {
  firstName: 'Adam',
  lastName: 'Borek'
}

Type declaration

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',
}
  1. member1 is not actually a Member. It lacks id, a required field for Member type.
  2. While writing member2 details I got all Typescript auto-completion working! That was not the case for memebr1

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:

  1. Declare a variable as Partial<XYZ> or PartialDeep<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 here foo(partial as YOUR_ORIGINAL_TYPE)
  2. 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 ❤️?