Getting friendly with TypeScript (part 5): keyof, typeof and extends
That’s not the most intuitive description right there, so let’s jump straight into some code to understand this one.
We start off with a simple object type called… ObjectType .
This type requires two keys ( "x" and "y” ).
The value of these keys has to be of type number (e.g. "x": 9, "y": 10 )
In line 2, we use the keyof operator. Here, we basically ask for the keys ( "x" and "y" ) from OperatorType , then TypeScript maps through these keys and makes a literal union type from them.
Literal means that the type has to match the keys exactly.
(…and we learned about union types in part 1)
In the example above, this means that anything of type KeysOfExample can only be "x" or "y" — a literal union.
TypeScript helps us out with IntelliSense as you can see in line 6.
Below is another example, but this time the keys are "red” , "blue” , and "yellow” — another literal union:
When to use it?
The keyof operator is useful for mapping, and when you want the keys of a certain type to be specific values only.
TypeScript definition:
“JavaScript already has a typeof operator you can use in an expression context. TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or property.”
This description is easier to comprehend but let’s dive back into the code to understand it anyway.
In line 15, I assign the value "Sammy" to the variable myName
In line 16, I then assign typeof myName to the type MyName
What do you think the value of MyName will be here?
When you hover over type MyName you see that it is equal to string
This means that anything of type MyName has to be of type string .
When to use it?
Now, you might be thinking that this is a pretty simple operator, but it can be really helpful in certain scenarios.
Example 1: typeof as a type guard ♀️
Take the following code, for example:
Here, we try to create a function called handleStringOrNumber which takes either a string or number as an argument (making x a Union type), and then try to console log the result of x.toUpperCase … can you see why we might get an error here?
If we hover over the error, TypeScript tells us that toUpperCase does not exist on type number . Although x is an Union type that can be of type string or number , we don’t know which type will be input at runtime and so we need to create type guards to check this.
In the screenshot above, we use typeof to check what the input type is before we do anything with it. Now, we don’t see an error when using toUpperCase because we check first if the input type is string and thus check whether the toUpperCase method can be applied to the variable.
Example 2: typeof with return types ⏎
Using TypeScript’s own documentation example, typeof can be useful when dealing with ReturnType<T>
ReturnType<T> is a built-in TypeScript type and it does exactly what the name says: it takes a function type and returns its return type.
Here, functionReturnExample is equal to number , which means that any function with this type has to return a number.
Let’s try using ReturnType<T> with a named function:
You can see we get an error.
The error message is really helpful here. It tells us that we are using the return value of namedFunction incorrectly. A value can’t be used as a type.
Thinking about what you’ve read so far, how do you think we can fix this? (hint: we use typeof … 😉 )
By adding typeof before namedFunction in the ReturnType argument, we return the type of the function (which is string ) instead of the value (which would be "I’m a named function” ).
The error is gone (hurray!), and when we hover over namedFunctionReturnType we see type namedFunctionReturnType = string
If we now create a function and state that its return type should be namedFunctionReturnType (line 7 in the screenshot below), we have to return a type string . In the example below, we try to return a number and get an error.
Woo! So typeof is a pretty self-explanatory operator, but it can be useful for type guarding, and when working with functions in TypeScript.
What does it do?
TypeScript definition:
“The extends keyword on an interface allows us to effectively copy members from other named types and add whatever new members we want.”
You know what’s coming… let’s look at this in the code.
In the image above, we have two interfaces that are exactly the same, except EvolvedPokemon has one more property called evolvesFrom
This seems a bit repetitive, right? Right.
That’s where the extends keyword helps us:
Look at that code clean-up . Here, the extends keyword ensures EvolvedPokemon gets all the same properties as Pokemon and also adds the evolvesFrom property. It’s exactly the same as the previous example, but far more efficient.
Enjoy this nostalgic example of an extends type in action:
We can also use the extends keyword to merge the properties of two types or interfaces into a new interface.
Note: when we use use the extends keyword we have to create a new interface and not a type. To combine two types into another type we can use Intersection types.
Above, we have two interfaces. We want to make a new interface with the properties of both of these interfaces. Using the previous example as a reference, how do you think we achieve this?
The example above creates a new interface called Product which takes the properties of both Device and Brand . iPhone is of type Product and therefore requires the properties deviceType and brandName
When to use it?
I think this one might be more intuitive, but we use extends when we want to create a new interface that takes properties of an already existing type or interface and then adds more properties.
We can also use extends to combine the properties of types and/or interfaces into a new interface. The main purpose of extends is to create more specific types while avoiding code repetition.
If you enjoyed this article, you might enjoy other articles from the ‘Getting friendly with…’ series:
Keep challenging yourself and remember, being uncomfortable means you are growing.
Narrowing
If padding is a number , it will treat that as the number of spaces we want to prepend to input . If padding is a string , it should just prepend padding to input . Let’s try to implement the logic for when padLeft is passed a number for padding .
Uh-oh, we’re getting an error on padding . TypeScript is warning us that we’re passing a value with type number | string to the repeat function, which only accepts a number , and it’s right. In other words, we haven’t explicitly checked if padding is a number first, nor are we handling the case where it’s a string , so let’s do exactly that.
If this mostly looks like uninteresting JavaScript code, that’s sort of the point. Apart from the annotations we put in place, this TypeScript code looks like JavaScript. The idea is that TypeScript’s type system aims to make it as easy as possible to write typical JavaScript code without bending over backwards to get type safety.
While it might not look like much, there’s actually a lot going on under the covers here. Much like how TypeScript analyzes runtime values using static types, it overlays type analysis on JavaScript’s runtime control flow constructs like if/else , conditional ternaries, loops, truthiness checks, etc., which can all affect those types.
Within our if check, TypeScript sees typeof padding === «number» and understands that as a special form of code called a type guard. TypeScript follows possible paths of execution that our programs can take to analyze the most specific possible type of a value at a given position. It looks at these special checks (called type guards) and assignments, and the process of refining types to more specific types than declared is called narrowing. In many editors we can observe these types as they change, and we’ll even do so in our examples.
There are a couple of different constructs TypeScript understands for narrowing.
typeof type guards
As we’ve seen, JavaScript supports a typeof operator which can give very basic information about the type of values we have at runtime. TypeScript expects this to return a certain set of strings:
- «string»
- «number»
- «bigint»
- «boolean»
- «symbol»
- «undefined»
- «object»
- «function»
Like we saw with padLeft , this operator comes up pretty often in a number of JavaScript libraries, and TypeScript can understand it to narrow types in different branches.
In TypeScript, checking against the value returned by typeof is a type guard. Because TypeScript encodes how typeof operates on different values, it knows about some of its quirks in JavaScript. For example, notice that in the list above, typeof doesn’t return the string null . Check out the following example:
In the printAll function, we try to check if strs is an object to see if it’s an array type (now might be a good time to reinforce that arrays are object types in JavaScript). But it turns out that in JavaScript, typeof null is actually «object» ! This is one of those unfortunate accidents of history.
Users with enough experience might not be surprised, but not everyone has run into this in JavaScript; luckily, TypeScript lets us know that strs was only narrowed down to string[] | null instead of just string[] .
This might be a good segue into what we’ll call “truthiness” checking.
Truthiness might not be a word you’ll find in the dictionary, but it’s very much something you’ll hear about in JavaScript.
In JavaScript, we can use any expression in conditionals, && s, || s, if statements, Boolean negations ( ! ), and more. As an example, if statements don’t expect their condition to always have the type boolean .
In JavaScript, constructs like if first “coerce” their conditions to boolean s to make sense of them, and then choose their branches depending on whether the result is true or false . Values like
- 0
- NaN
- «» (the empty string)
- 0n (the bigint version of zero)
- null
- undefined
all coerce to false , and other values get coerced to true . You can always coerce values to boolean s by running them through the Boolean function, or by using the shorter double-Boolean negation. (The latter has the advantage that TypeScript infers a narrow literal boolean type true , while inferring the first as type boolean .)
It’s fairly popular to leverage this behavior, especially for guarding against values like null or undefined . As an example, let’s try using it for our printAll function.
You’ll notice that we’ve gotten rid of the error above by checking if strs is truthy. This at least prevents us from dreaded errors when we run our code like:
Keep in mind though that truthiness checking on primitives can often be error prone. As an example, consider a different attempt at writing printAll
We wrapped the entire body of the function in a truthy check, but this has a subtle downside: we may no longer be handling the empty string case correctly.
TypeScript doesn’t hurt us here at all, but this behavior is worth noting if you’re less familiar with JavaScript. TypeScript can often help you catch bugs early on, but if you choose to do nothing with a value, there’s only so much that it can do without being overly prescriptive. If you want, you can make sure you handle situations like these with a linter.
One last word on narrowing by truthiness is that Boolean negations with ! filter out from negated branches.
TypeScript also uses switch statements and equality checks like === , !== , == , and != to narrow types. For example:
When we checked that x and y are both equal in the above example, TypeScript knew their types also had to be equal. Since string is the only common type that both x and y could take on, TypeScript knows that x and y must be a string in the first branch.
Checking against specific literal values (as opposed to variables) works also. In our section about truthiness narrowing, we wrote a printAll function which was error-prone because it accidentally didn’t handle empty strings properly. Instead we could have done a specific check to block out null s, and TypeScript still correctly removes null from the type of strs .
JavaScript’s looser equality checks with == and != also get narrowed correctly. If you’re unfamiliar, checking whether something == null actually not only checks whether it is specifically the value null — it also checks whether it’s potentially undefined . The same applies to == undefined : it checks whether a value is either null or undefined .
The in operator narrowing
JavaScript has an operator for determining if an object or its prototype chain has a property with a name: the in operator. TypeScript takes this into account as a way to narrow down potential types.
For example, with the code: «value» in x . where «value» is a string literal and x is a union type. The “true” branch narrows x ’s types which have either an optional or required property value , and the “false” branch narrows to types which have an optional or missing property value .
To reiterate, optional properties will exist in both sides for narrowing. For example, a human could both swim and fly (with the right equipment) and thus should show up in both sides of the in check:
JavaScript has an operator for checking whether or not a value is an “instance” of another value. More specifically, in JavaScript x instanceof Foo checks whether the prototype chain of x contains Foo.prototype . While we won’t dive deep here, and you’ll see more of this when we get into classes, they can still be useful for most values that can be constructed with new . As you might have guessed, instanceof is also a type guard, and TypeScript narrows in branches guarded by instanceof s.
As we mentioned earlier, when we assign to any variable, TypeScript looks at the right side of the assignment and narrows the left side appropriately.
Notice that each of these assignments is valid. Even though the observed type of x changed to number after our first assignment, we were still able to assign a string to x . This is because the declared type of x — the type that x started with — is string | number , and assignability is always checked against the declared type.
If we’d assigned a boolean to x , we’d have seen an error since that wasn’t part of the declared type.
Control flow analysis
Up until this point, we’ve gone through some basic examples of how TypeScript narrows within specific branches. But there’s a bit more going on than just walking up from every variable and looking for type guards in if s, while s, conditionals, etc. For example
padLeft returns from within its first if block. TypeScript was able to analyze this code and see that the rest of the body ( return padding + input; ) is unreachable in the case where padding is a number . As a result, it was able to remove number from the type of padding (narrowing from string | number to string ) for the rest of the function.
This analysis of code based on reachability is called control flow analysis, and TypeScript uses this flow analysis to narrow types as it encounters type guards and assignments. When a variable is analyzed, control flow can split off and re-merge over and over again, and that variable can be observed to have a different type at each point.
Using type predicates
We’ve worked with existing JavaScript constructs to handle narrowing so far, however sometimes you want more direct control over how types change throughout your code.
To define a user-defined type guard, we simply need to define a function whose return type is a type predicate:
pet is Fish is our type predicate in this example. A predicate takes the form parameterName is Type , where parameterName must be the name of a parameter from the current function signature.
Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.
Notice that TypeScript not only knows that pet is a Fish in the if branch; it also knows that in the else branch, you don’t have a Fish , so you must have a Bird .
You may use the type guard isFish to filter an array of Fish | Bird and obtain an array of Fish :
In addition, classes can use this is Type to narrow their type.
Types can also be narrowed using Assertion functions.
Most of the examples we’ve looked at so far have focused around narrowing single variables with simple types like string , boolean , and number . While this is common, most of the time in JavaScript we’ll be dealing with slightly more complex structures.
For some motivation, let’s imagine we’re trying to encode shapes like circles and squares. Circles keep track of their radiuses and squares keep track of their side lengths. We’ll use a field called kind to tell which shape we’re dealing with. Here’s a first attempt at defining Shape .
Notice we’re using a union of string literal types: «circle» and «square» to tell us whether we should treat the shape as a circle or square respectively. By using «circle» | «square» instead of string , we can avoid misspelling issues.
We can write a getArea function that applies the right logic based on if it’s dealing with a circle or square. We’ll first try dealing with circles.
Under strictNullChecks that gives us an error — which is appropriate since radius might not be defined. But what if we perform the appropriate checks on the kind property?
Hmm, TypeScript still doesn’t know what to do here. We’ve hit a point where we know more about our values than the type checker does. We could try to use a non-null assertion (a ! after shape.radius ) to say that radius is definitely present.
But this doesn’t feel ideal. We had to shout a bit at the type-checker with those non-null assertions ( ! ) to convince it that shape.radius was defined, but those assertions are error-prone if we start to move code around. Additionally, outside of strictNullChecks we’re able to accidentally access any of those fields anyway (since optional properties are just assumed to always be present when reading them). We can definitely do better.
The problem with this encoding of Shape is that the type-checker doesn’t have any way to know whether or not radius or sideLength are present based on the kind property. We need to communicate what we know to the type checker. With that in mind, let’s take another swing at defining Shape .
Here, we’ve properly separated Shape out into two types with different values for the kind property, but radius and sideLength are declared as required properties in their respective types.
Let’s see what happens here when we try to access the radius of a Shape .
Like with our first definition of Shape , this is still an error. When radius was optional, we got an error (with strictNullChecks enabled) because TypeScript couldn’t tell whether the property was present. Now that Shape is a union, TypeScript is telling us that shape might be a Square , and Square s don’t have radius defined on them! Both interpretations are correct, but only the union encoding of Shape will cause an error regardless of how strictNullChecks is configured.
But what if we tried checking the kind property again?
That got rid of the error! When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.
In this case, kind was that common property (which is what’s considered a discriminant property of Shape ). Checking whether the kind property was «circle» got rid of every type in Shape that didn’t have a kind property with the type «circle» . That narrowed shape down to the type Circle .
The same checking works with switch statements as well. Now we can try to write our complete getArea without any pesky ! non-null assertions.
The important thing here was the encoding of Shape . Communicating the right information to TypeScript — that Circle and Square were really two separate types with specific kind fields — was crucial. Doing that lets us write type-safe TypeScript code that looks no different than the JavaScript we would’ve written otherwise. From there, the type system was able to do the “right” thing and figure out the types in each branch of our switch statement.
As an aside, try playing around with the above example and remove some of the return keywords. You’ll see that type-checking can help avoid bugs when accidentally falling through different clauses in a switch statement.
Discriminated unions are useful for more than just talking about circles and squares. They’re good for representing any sort of messaging scheme in JavaScript, like when sending messages over the network (client/server communication), or encoding mutations in a state management framework.
When narrowing, you can reduce the options of a union to a point where you have removed all possibilities and have nothing left. In those cases, TypeScript will use a never type to represent a state which shouldn’t exist.
The never type is assignable to every type; however, no type is assignable to never (except never itself). This means you can use narrowing and rely on never turning up to do exhaustive checking in a switch statement.
For example, adding a default to our getArea function which tries to assign the shape to never will raise an error when every possible case has not been handled.
Typeof Type Operator
JavaScript already has a typeof operator you can use in an expression context:
TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or property:
This isn’t very useful for basic types, but combined with other type operators, you can use typeof to conveniently express many patterns. For an example, let’s start by looking at the predefined type ReturnType<T> . It takes a function type and produces its return type:
If we try to use ReturnType on a function name, we see an instructive error:
Remember that values and types aren’t the same thing. To refer to the type that the value f has, we use typeof :
TypeScript intentionally limits the sorts of expressions you can use typeof on.
Types from Extraction
TypeScript’s type system is very powerful because it allows expressing types in terms of other types. Although the simplest form of this is generics, we actually have a wide variety of type operators available to us. It’s also possible to express types in terms of values that we already have.
By combining various type operators, we can express complex operations and values in a succinct, maintainable way. In this chapter we’ll cover ways to express a type in terms of an existing type or value.
The typeof type operator
JavaScript already has a typeof operator you can use in an expression context:
TypeScript adds a typeof operator you can use in a type context to refer to the type of a variable or property:
This isn’t very useful for basic types, but combined with other type operators, you can use typeof to conveniently express many patterns. For an example, let’s start by looking at the predefined type ReturnType<T> . It takes a function type and produces its return type:
If we try to use ReturnType on a function name, we see an instructive error:
Remember that values and types aren’t the same thing. To refer to the type that the value f has, we use typeof :
Limitations
TypeScript intentionally limits the sorts of expressions you can use typeof on. Specifically, it’s only legal to use typeof on identifiers (i.e. variable names) or their properties. This helps avoid the confusing trap of writing code you think is executing, but isn’t:
The keyof type operator
The keyof operator takes a type and produces a string or numeric literal union of its keys:
If the type has a string or number index signature, keyof will return those types instead:
Note that in this example, M is string | number — this is because JavaScript object keys are always coerced to a string, so obj[0] is always the same as obj[«0»] .
keyof types become especially useful when combined with mapped types, which we’ll learn more about later.
Indexed Access Types
We can use typeof to reference the type of a property of a value. What if we want to reference the type of a property of a type instead?
We can use an indexed access type to look up a specific property on another type:
The indexing type is itself a type, so we can use unions, keyof , or other types entirely:
You’ll even see an error if you try to index a property that doesn’t exist:
Another example of indexing with an arbitrary type is using number to get the type of an array’s elements. We can combine this with typeof to conveniently capture the element type of an array literal: