Skip to content

Instantly share code, notes, and snippets.

@vinhlee95
Last active January 17, 2022 06:02
Show Gist options
  • Save vinhlee95/203224beb49988a9c5146edb4f00629b to your computer and use it in GitHub Desktop.
Save vinhlee95/203224beb49988a9c5146edb4f00629b to your computer and use it in GitHub Desktop.
The unknown type in TypeScript

The unknown type in TypeScript

The unknown mistery

When I first onboarded TypeScript world from JavaScript, one of the most confusing thing is the unknown type. Remember when your coworkers write something like:

const foo = () => bar as unknown as any // <- πŸ€·πŸ»β€β™‚οΈπŸ€·πŸ»β€β™‚οΈπŸ€·πŸ»β€β™‚οΈ

Let's take a look at TypeScript official documentation:

unknown is the type-safe counterpart of any.
Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing.

Does this clarify or just further confuse you? I know we don't like boring texts and documentation, so let's translate those into actual code:

// Anything is assignable to unknown
const foo: unknown = 1 // <- we can assign a number to a unknown var
const bar: unknown = true // <- same with other primitive types such as boolean

// But unknown isn’t assignable to anything but itself
const myString: string = foo // <- ⛔️⛔️⛔️ unknown type is not assignable to string type
const isGreaterThan10 = (val: unknown) => val > 10 // <- ⛔️⛔️⛔️ because val has unknown type

Notice that even after being assigned to a number-typed value, foo still has unknown type and thus cannot do number-specific stuffs:

const foo: unknown = 1
console.log(foo > 10) //  <- ⛔️⛔️⛔️ foo is still unknown, so it cannot be compared to a number

You can however make the compiler happy by casting the type of foo to number like:

// Rest assure compiler, I know what I am doing with foo
console.log(foo as number > 10)

In this trivial example, there is not much risk because we already assigned foo = 1 in the first place. However, in more complex cases, casting the type like this might backfire us:

const getKeys = (obj: unknown) => {
  return Object.keys(obj as object) // <- cast the type to be object
}

const getObject = (): object | undefined => {
  // some code that return EITHER an object OR undefined πŸ’£πŸ’£πŸ’£
}

getKeys(getObject()) // <- πŸ’£πŸ’₯ when getObject() returns undefined

Alright that's enough for the basics, now let me show you the most common real-world usages of the unknown type and how to safely handle it πŸ’ͺπŸ’ͺπŸ’ͺ

Handle external data structure

We need to be mindful where getting and parsing these data sources such as 3rd party JSON responses. An analogy for this is when you received a "gift" from some strangers. You almost always want to check it carefully before actually using.

Let's say we want to fetch a dog by its id from a 3rd party API:

const dog = await fetchDogByIdFromApi(1)

Could we know what type this dog has? πŸ€” Most likely NOT before we could do some checks.

So what should we do? We should consider it to have unknown type ❓. By doing this, TypeScript will prevent us from freely assigning its value such as:

type Dog = {name: string}
const fetchedDog: unknown = await fetchDogByIdFromApi(1)
const ourDog: Dog = fetchedDog //  ⛔️⛔️⛔️ <- unknown could not be assigned to Dog!

The compiler also does not allow us to carelessly read the data like:

const ourDogName = fetchedDog.name.toLowerCase() // πŸ’£β›”οΈβ›”οΈ <- this will explode if fetchedDog.name does not exist

Pretty useful isn't it? You must have additional checks to be able to assign an unknown value to our known Dog-typed variable. This totally make sense. Otherwise what would be the benefits of TypeScript if we just freely assign a not-yet-known value to a variable? πŸ˜… This is probably 1 of the most common source for the timeless error: TypeError: Cannot Read Property of Undefined.

So how could we safely handle unknown type and let TypeScript at ease? Here are 2 approaches that I often go to: typeguards and non-null assertions.

Typeguards

Typeguard is an expression - an additional check to assure TypeScript that an unknown value conforms to a type definition.

Let's see how can we write a typeguard to safely use fetchedDog value as our own Dog:

const isDog = (value: unknown): value is Dog => !!value && typeof value === 'object' && 'name' in value && typeof (value as Dog).name === 'string'

In this typeguard, we are using type predicate - a special return types that tells TypeScript compiler that as long as value satisfies inner checks, it must have Dog type.

Note that it is safe to use typecast value as Dog in this case to access name property because we already checked type value === 'object' && 'name' in value

With this in place, we can totally treat the newbie fetchedDog as 1 of our own:

if(isDog(fetchedDog)) {
  const ourDogName = fetchedDog.name.toLowerCase() // βœ…βœ…βœ… <- all good, isDog is sufficient in making sure that fetchedDog has `Dog` type
} else {
  throw new Error('error in parsing fetched value to Dog type')
}

Non-null assertions

An alternative to typeguard is an assertion function to throw an error if the input does not conform to our Dog type:

type AssertDogFn = (value: unknown) => asserts value is Dog
const assertDog: AssertDogFn = (value) => {
  if(!value || typeof value !== 'object' || 'name' in value === false || (typeof (value as Dog).name !== 'string')) {
    throw new Error('error in parsing fetched value to Dog type')
  }
}

This assertDog function does basically what isDog typeguard did. The only difference is the signature where it throw an Error right away if the type is mismatched. Therefore, we don't need an if/else statement in the calling code:

assertDog(fetchedDog)
const lowerCaseName = fetchedDog.name.toLowerCase() // βœ…βœ…βœ… <- all good, if fetchedDog is not a `Dog`, assertDog should have already thrown

Key takeaways

unknown is a pretty great feature of TypeScript in my opinion. It proves extremely useful when we need to parse, well unknown, 3rd party data sources.

To summary what we have gone through in this blog:

  • Anything is assignable to unknown, but unknown is not assignable to anything but itself. So we can do const foo: unknown = 'foo', but we cannot do, out of the box, const bar: string = foo if foo is unknown.
  • We can force the compiler to trust that an unknown varible has a specific type: const bar: string = foo as string. However, typecast might backfire us if we are not mindful using it.
  • Typeguards or non-null assertions are really powerful expressions that perform runtime checks to guarantee the type of an unknown value. They are generally better and safer to use that typecasts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment