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 tounknown
, butunknown
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 πͺπͺπͺ
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.
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')
}
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
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
, butunknown
is not assignable to anything but itself. So we can doconst foo: unknown = 'foo'
, but we cannot do, out of the box,const bar: string = foo
iffoo
isunknown
. - 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.