We have different types of named things in TypeScript, including types, variables and functions (and occasionally things that can be used as both). By the end of this chapter, you’ll have a solid understanding of how to examine and understand these entities in TypeScript.
In order to truly understand how types and values “stack” on each other, we’ll first tackle the concept of declaration merging. Often when people grasp how TypeScript handles this, they never look at the language the same way again.
Many things can be declared with a name and referenced later in the TypeScript world, this includes variables and types as we can see below
tsTry
interfaceFruit {name : stringmass : numbercolor : string}constbanana :Fruit = {name : "banana",color : "yellow",mass : 183,}// both of these things are exportableexport {banana ,Fruit }
Let’s coin a term here and call banana
and Fruit
both identifiers1, in that they provide a named and exportable reference to some information (be it a value, or a type)
Stacking multiple things on an identifier
It may seem a little silly, but what if we built a function called Fruit
that returned banana
-shaped objects? What do you think would happen?
tsTry
interfaceFruit {name : stringmass : numbercolor : string}functionFruit (kind : string) {switch (kind ) {case "banana": returnbanana default: throw newError (`fruit type ${kind } not supported`)}}export {Fruit }
It’s probably surprising for some readers that this is not throwing a compiler error, as would be the case if we declared two types or two values of the same name. The tooltip on the export
is particularly interesting. There’s a lot more going on here.
Let’s introduce one more thing to this situation: a namespace
with the same Fruit
name. We’ll talk more about namespace
s later
tsTry
interfaceFruit {name : stringmass : numbercolor : string}functionFruit (kind : string) {switch (kind ) {case "banana": returnbanana default: throw newError (`fruit type ${kind } not supported`)}}// the namespacenamespaceFruit {functioncreateBanana ():Fruit {returnFruit ('banana')}}export {Fruit }
We can learn a couple of things from this situation. First, what we see around export { Fruit }
is that there’s identifier that’s three things in one:
- a value (class)
- a type
- a namespace
Second, we can see that when Fruit
is used in a place where we expect to see type information, we see the interface
and namespace
information on the tooltip. When Fruit
is used in a place where we expect to see a value, we see the function
and namespace
information. It appears there’s something at play that involves using single identifier in different contexts.
How to tell what’s on an identifier
Tooltips, and attempts to use identifiers in certain positions are a great mechanism of understanding what we’re dealing with on an identifier.
tsTry
constis_a_value = 4typeis_a_type = {}namespaceis_a_namespace {constfoo = 17}// how to test for a (value | namespace)constx =is_a_value // the value position (RHS of =).// how to test for a typeconsty :is_a_type = {} // the type position (LHS of =).// how to test for a namespace (hover over is_a_namespace symbol)is_a_namespace
Let’s look at some failing cases to convince ourselves that these tests work
tsTry
constis_a_value = 4typeis_a_type = {}namespaceis_a_namespace {constfoo = 17}// how to test for a valueconst'is_a_type' only refers to a type, but is being used as a value here.2693'is_a_type' only refers to a type, but is being used as a value here.x =is_a_type constxx =is_a_namespace // how to test for a typeconst'is_a_value' refers to a value, but is being used as a type here. Did you mean 'typeof is_a_value'?2749'is_a_value' refers to a value, but is being used as a type here. Did you mean 'typeof is_a_value'?y := {} is_a_value constCannot use namespace 'is_a_namespace' as a type.2709Cannot use namespace 'is_a_namespace' as a type.yy := { } is_a_namespace
Note that namespace
passes the “value test” — as we’ll learn in a moment, it is a value.
A short aside: what’s the point namespace
?
Do any of you remember using jQuery?
To describe the way this library works using type information, you need to be able to handle cases like
tsTry
// a `fetch` kind of function$ .ajax ({url : "/api/getWeather",data : {zipcode : 97201,},success : function (result ) {$ ("#weather-temp")[0].innerHTML ="<strong>" +result + "</strong> degrees"},})// a `document.querySelectorAll` kind of function$ ("h1.title").forEach ((node ) => {node .tagName // "h1"})
We could define a function and a namespace that “stack” like this, so that $
could simultaneously be invoked directly, and serve as a namespace for things like $.ajax
, $.getJSON
and so on…
tsTry
function$ (selector : string):NodeListOf <Element > {returndocument .querySelectorAll (selector )}namespace$ {export functionajax (arg : {url : stringdata : anysuccess : (response : any) => void}):Promise <any> {returnPromise .resolve ()}}
Generally, writing code in this way is a bit outdated, left over from the days where we didn’t have modules, and installed libraries as global variables. With this in mind, let’s not give namespace
too much more thought for now.
A look back on class
With our new knowledge of “things that can stack on an identifier”, let’s take another close look at a class
in TypeScript
tsTry
classFruit {name ?: stringmass ?: numbercolor ?: stringstaticcreateBanana ():Fruit {return {name : "banana",color : "yellow",mass : 183 }}}
and let’s apply our type
and value
tests to this Fruit
identifier
tsTry
// how to test for a valueconstvalueTest =Fruit // Fruit is a value!valueTest .createBanana // how to test for a typelettypeTest :Fruit = {} as any // Fruit is a type!typeTest .color
So it seems that classes are both a type and a value.
The word completions for the letter c
above are a clue as to what’s going on:
- When
Fruit
is used as a type, it describes the type of an instance of Fruit - When
Fruit
is used as a value, it can both act as the constructor (e.g.,new Fruit()
) and holds the “static side” of the class (createBanana()
in this case)
It turns out we’ve already been benefiting from declaration merging this whole time!
-
TypeScript internally calls this a
↩ts.Symbol
, not to be confused with the JavaScript concept of the same name.