Type Inference
In TypeScript, when initializing a variable with a value, we do not necessarily need to provide the data type for the variable. The TypeScript compiler is smart enough to deduce (infer) the type by looking at the type and shape of the value we are assigning to the variable in the declaration.
In the above example, we have declared a few variables without an explicit type but with an initial value. When you hover over a variable, TypeScript provides you the interfered type which I have shown you in the comments.
If you take a look carefully, you can see TypeScript was able to tell the function argument types and return value type based on the default argument value and the return statement. This is some clever stuff.
The process of deducing or inferring type from the initial value is called type inference. TypeScript can infer the type of a variable or a constant from its initial value, type of an argument from its default value, and the return type of a function from the return value. TypeScript can also infer types in other situations as we will see in the next sections.
TypeScript, however, is not wise enough to infer complex types. For example, it won’t be able to tell if an object is a type of interface by looking at the shape of the object. We need to provide that information beforehand. However, that is not necessary and we will learn in a bit why that’s the case.
Type Assertion
In most programming languages, type casting transforms a value from one format to another format. For example, a string
value such as "3"
can be converted to a number
value 3
using parseInt
function. This is also called type conversion since we are converting a value of a one data type to a value of another data type.
Type Assertion on the other hand is a compile-time feature. In TypeScript, we use type assertion to instruct the TypeScript compiler to treat a value as a different type than it originally is. For example, you can ask the TypeScript compiler to treat 3
as a string
and TypeScript will treat it as a string
giving you fancy autocomplete and IntelliSense. It doesn’t impact the type of the value at runtime since it doesn’t transform or mutate the value.
This is quite helpful when the type of a value is any
but we know for sure that the type of the value is something else and we want all the fancy auto-complete and IntelliSense features provided by the IDE for that value. Type assertion can be used whenever you want but with care.
In TypeScript, we use value as Type
syntax to tell the compiler to treat the value value
as the type of Type
. We can also use <Type>value
syntax which does the same thing. We might need to use parenthesis in type assertion syntax in some situations such as (<number>someNumStr).toFixed(n)
or (someNumStr as number).toFixed(n)
.
In the above example, the getStuff
function returns a value of type any
. The any
type is one of the most uninteresting types since it doesn’t carry any information about the value. Hence when we call toFixed()
method on apple
which is a string
at runtime but any
at compile-time, the TypeScript compiler doesn’t warn us since for it any
can have any shape.
The above program compiles but when you run this program, it obviously crashes with some TypeError
errors as shown in the above example. However, we can fix this by using type assertions.
In the modified example above, we have explicitly asserted that the type of apple
is string
and type of pi
is a number
before calling the type-specific methods. Using this information, the IDE and the TypeScript compiler know what they are dealing with.
Type assertion is very helpful when it comes to type unions which is an abstract type and we will look into this in a bit. Type assertion gives a lot of power but with great power comes great responsibility. Tricking the TypeScript compiler into believing something (the type of a value) which is not in real life (at runtime) can be devastating.
Literal Types
We have seen string
, number
, boolean
and other primitive types as well as interface
, function
abstract types. As we have just learned, TypeScript can infer the type from the initial value. So far, we have used let
keyword to declare a variable, but is the story the same for the const
keyword?
In the above example, we have defined constant s
which has a string
value and constant n
which has a number
value. If you hover over these entities, you will see something peculiar. Their inferred types seem weird. They are displayed in the comments above.
With var
or let
, the TypeScript compiler expects these values to change during the program execution. Hence types like string
or number
seems more suitable. However, a constant will never change, therefore having these collective types really doesn’t make any sense.
Hence TypeScript uses the value itself as a type which can you can see in the comments of the above example. These are called literal types since the type itself is the literal value assigned.
These are also called unit types since they represent a single value out of an infinite set of literal values. We can define literal types for string
, number
and boolean
, however, boolean
can only have two unit types.
This isn’t just in the case of const
declaration though. You can literally enforce a variable, function argument, or function return value to have a literal type. Let’s see an example of this.
In the above example, we have defined a function executeSafe
that always returns 0
which could be the status code of the task execution. This means the task always succeeds with 0
status code.
So instead of giving the return type of number
, we have given return types of 0
. Later down the program, we tried to compare if the return value of this function is equal to 1
which is illegal since this condition will never happen.
TypeScript compiler complains where a unit type is expected but a variable of a collective type such as string
is provided, even though the variable has the value of unit type. This is because the variable can contain any value at the runtime. To solve this, we need to use type assertion, as explained below.
Literal types are more useful while defining a set of possible literal values. This can be done using a type union as described below.
💡 TypeScript prefers the unit type over a collective type in some situations. For example, if you pass a string value such as
'hello'
as an argument which is a type ofstring
, TypeScript uses its literal type'hello'
as the type for the function call. This might be later coerced into something broader such asstring
orany
based on the parameter type of the function. This process of narrowing a type from a collective type to a unit type is called type narrowing.
Type Union
A type union is a union of two or more types. An easy way to understand unions is to imagine a set of possible literal types. For example, if gender
variable has to be 'male'
, 'female'
or 'other'
, then using string
type for it would mean that we’ll need to do value check at runtime.
In the above program, we have written a function setGender
whose sole purpose is to accept a gender
value of type string
and do something with it. But since gender
can be only one of these three values, we need to check if the user has provided the inappropriate value at runtime.
To avoid this, we can use literal types. The individual literal types for the gender
argument are 'male'
, 'female'
and 'other'
. A single unit type can’t help here since we need gender
argument value to be the of type 'male'
OR 'female'
OR 'other'
. This is where the union type comes in.
The pipe (|)
operator combines one or more types together to form a new type. This is a kind of logical OR operation but with types. Now, we can modify the above program using the union of these literal types.
Now in the modified program, the gender
argument has the union type of 'male' | 'female' | 'other'
. With this information, the TypeScript compiler will fail to compile the program since the setGender
function call was made with the 'true'
argument value which has the type of 'true'
(type narrowing) and it is not present in the specified union type. This also helps us eliminate unnecessary code to verify the value of the gender
argument at runtime since the program won’t compile otherwise.
A union type can be created from almost all types. For example, if the type of a variable can be either string
or a number
in the runtime, then you can create string | number
type which will allow a number
or string
value.
A union type can also be created from two or more interfaces. A union of interfaces can be used to let a value pass by which has the shape of either of the interfaces in the union. However, things can get a little complicated.
In the example above, we have defined Student
interface and Player
interface. These interfaces have property name
in common. The printInfo
function accepts an argument of type Student
or Player
.
This program will fail to compile since we are trying to access property marks
on the argument person
which could either be Student
or Player
at runtime and property marks
does not exist on the Player
. That’s not the case with property name
though since it exists on both types.
We can solve this issue is using type assertion. We can instruct the TypeScript compiler that the argument value at runtime will be of a particular type, however, that’s a bold move. This is explained below.
In the above example, the (person as Student)
type assertion coerce the type of person
from Student | Player
to Student
. But if we pass an argument that does not have marks
property, the result could be devastating.
You can obviously write a better logic to handle such a scenario but in the end, you would need to use the type assertion syntax to instruct the TypeScript compiler what type you are expecting.
Discriminated Unions
However, TypeScript provides a neat logic to discriminate types in a union automatically without having to use type assertion. What we need is a common field in all the interfaces of a union that can be used to distinguish that type. The type of this field must be a unique literal type.
In the above program, we have used the field type
in the interface as a discriminant to provide TypeScript compiler the information to discriminate types in the union. However, we need to use a type guard (explained later) to enable that.
In this case, we have used switch
statement as a type guard that identifies the type of interface based on person.type
which is the discriminant. The TypeScript compiler uses this information to discriminate the type and provides the correct type inside case
blocks as shown in the comments. You would be able to see these types when you hover over the person
argument in a case
block. This also brings you the magic of IntelliSense.
The keyof operator
TypeScript provides the keyof
operator which creates a union of string
literal types from the keys of an interface. This is useful if a value should be one of the keys of an object, as explained in the following example.
Type union is quite helpful when a value can have more than one type at runtime. However, type union syntax can get long and hard to manage. Hence, it is better to create a type alias as shown below.
type Person = Student | Teacher | Coach
Now you can use Person
type instead of writing Student | Teacher | Coach
every time. Here the Person
is a type alias.
Type Guards
In the previous examples, we have used the switch/case
statement to resolve the type of person
argument which could either be the Student
or the Player
interface. We had to use a discriminant field using which each case
block scope gets the correct type of the person
argument.
Using the discriminant field value, the TypeScript compiler was able to narrow down the type of person
from the union of Student | Player
to Student
or Player
. The process of narrowing down the type of a value from a set of possibilities to a concrete type is called type narrowing and the expression that makes this possible is called a type guard.
A type guard’s job is to provide a definite type of a value in some scope. We can avoid unnecessary and dangerous type assertions just to compile the program if we know how to use type guards. Using type guards, the TypeScript compiler will be able to resolve the type of a value on its own and it will be safe to execute the program at runtime.
Using a discriminant field
As we saw earlier, we can use an interface field of literal type as a discriminant in a switch/case
statement to discriminate the type of a value from a set of possibilities (union). Here, the switch
statement forms a type guard using the discriminant field.
Using in operator
If types in a union have a property that never exists in the other types, then the value of that property can be used to distinguish the type. We generally use in
keyword in JavaScript to check if the object has a property.
If we have if/else
statement that checks if the value has a property using the in
operator, the TypeScript compiler narrows down that type in that if
block, provided that the property is unique among the given types in union.
In the above example, we have the Student
, Player
, and Racer
interface that describes the shape of an object. As we can see, the Student
interface has marks
property that does not exists in the other two. Similarly, the Player
and Racer
has score
and points
properties respectively of the same nature.
The printInfo
function accepts person
argument of type Student | Player | Racer
which means person
can be either of these at runtime. Here, we can use a type guard to discriminate the type of person
.
We have used if/else
block and in
operator to narrow down the type of person
from the union. Each if
block gets the type of person
based on the name of the property we are comparing it against.
The TypeScript compiler eliminates the resolved type from the union as it proceeds down the if/else
statement. Hence when it reaches the else
block, the union only has Racer
left in it. Hence the type of person
in the else
block is Racer
.
The type of person
argument outside if/else
statement remains the type of union unless a return statement is added in the if
block. If we add a return statement in a if
block, then we know for sure that code beneath it will only reach if person
has the type of union minus whatever types have been resolved so far. Hence, the else
block was not needed in the above example if every if
block had a return
statement in it.
Using instanceof operator
In the above example, our interfaces have a distinct property that can be used as a discriminant in a in
operator type guard. However, in some cases, objects have similar properties or there isn’t a distinct property that can be used as a discriminant. So type narrowing using in
operator can’t work.
If you are especially dealing with instances, then you can use instanceof
operator to check if an object is an instance of a class.
In the above example, the printInfo
function accepts the person
argument of type Student
or Player
. The instanceof
operator checks if the person
is an instance of Student
in the if
block. If this check validates, then the type of person
in this block is Student
. In the else
block, the person
has the type of Player
since it’s the only type left in the Student | Player
union after the first if
narrowing.
Using typeof operator
You can also discriminate types based on their type in JavaScript at runtime. As we know, we can use the typeof
operator to check the type of a value in JavaScript. If we use if/else
block with these operators, the TypeScript compiler will be able to narrow the types.
In the above example, we have a function printMarks
that accepts the marks
argument which could either be a string
or a number
which are JavaScript native types. Therefore, if we check the type of this property value using the typeof
operator (which returns the native type of a value) in the if
block, TypeScript can discriminate the type of the property in that if
block.
TypeScript can also discriminate the type of a property of an interface using the same principles as shown below. In this case, person.marks
have a definite type in each if
or else
blocks.
User-defined Type Guards
In situations where none of the above type guards can be implemented, there we can use type guard using is
keyword provided by the TypeScript. The is
keyword evaluates the actual type of the value against a type.
In the above example, we have created a predicateString
function that returns true
if arg
argument is a type of string
. The return type of this function is arg ``is`` string
which acts an indicator for the TypeScript compiler to coerce type of value
to string
if the return value is arg
is string
.
The arg is string
expression is called type predicate. In the printInfo
function, we have used the predicateString
function in the if/else
statement. Since the return type of predicateString
function is a type predicate, the TypeScript compiler can discriminate the type of marks
based on the return value, just like a type guard using typeof
operator.
💡 TypeScript can also use
value == null
expression for type narrowing. If you had a value of typenull | Student
then you can use this expression inif
block and the type ofvalue
in theelse
block will beStudent
automatically.
Type Intersection
In the Interfaces lesson, we saw interfaces inheriting from other interfaces. Using this feature, we can combine two or more shapes together which is great for mixins pattern. However, creating a new interface by extending two or more interfaces is not always practical.
Like unions, where a value can have a shape one of the given type in that union, intersection combines the shapes of two or more types together.
In the example above, we have created the Person
interface which contains firstName
and lastName
properties. The Player
interface represents the score
property and Student
interface represents the marks
property.
In a typical scenario, to represent an object that contains firstName
, lastName
and score
property, you would create a new interface, probably an empty one that extends both Person
and Player
.
However, TypeScript provides &
operator that combines two types together and returns a new type with properties of both the types. You can save these new types in an alias or use the same expression to represent a new type like what we have done in the example above.
The question that immediately pops in our mind is, what if there is a property that is common between two interfaces? Also, can I intersect primitive data types like number & string
together? Let’s take a look.
In the above example, Person
and Player
have a common gender
property, however, one has the string
type and other has the number
type. The TypeScript compiler doesn’t complain about this and it combines the two shapes together. However, the type of gender
property is set to never
.
This is because when two interfaces intersect, their common properties also intersect. Therefore, the resultant interface has a single property of the type resulted from the intersection between their original types. Therefore, the gender
property has the string & number
type after the intersection.
But string & number
intersection doesn’t make any sense since there is no value in the world that represents this intersection. Since there isn’t value that can be both string
and number
at the same time, this condition will never happen. Therefore, TypeScript automatically provides the never
type to the property instead of string & number
.
💡 If we have an intersection between a union and primitive data type then that union will be the result of the intersection.
Structural Typing
TypeScript, in a nutshell, is a type system for writing safe and better JavaScript programs. When we provide a type to an entity or coerce types using type assertion syntax, these types do not propagate into the runtime. Once the TypeScript program is compiled to JavaScript, these types get erased, hence it is called the erased type system.
Since values in TypeScript do not possess concrete types, type checking is done by looking at the shape of the values. Let’s take a simple example.
In the above example, class Person
and Student
shares the firstName
and lastName
properties. The getFullName
function accepts argument p
of interface type Person
and returns the full name by concatenating firstName
and lastName
property values.
Though we haven’t explicitly mentioned that the Student
class extends Person
class, TypeScript allowed monica
which is an instance of the class Student
as the valid value for the argument p
. This is because the monica
object also has firstName
and lastName
properties of type string
and that’s what TypeScript is interested in when it validates a value against Person
type.
This proves that TypeScript is a structurally typed language, also known as duck typing. As the saying goes, “If it walks like a duck, quacks like a duck, swims like a duck, then it must be a duck”. Since Student
type posses the behavior of Person
, TypeScript considers it as a Person
.
You can apply these principles beyond classes. Since a class type in TypeScript defines an implicit interface that contains all the public properties of that class (read more), you can use the same principle with interfaces.
The above example and the previous example are exactly similar. The only difference is that monica
and ross
are the plain JavaScript object of interface type while in the previous example, they were the instances of the classes so their types were inferred from the class.
This behavior sometimes also called structural subtyping. When the type A
has all properties of type B
, then A
is called as a subtype of B
. This is similar to inheritance in OOP. when a class A
extends class B
, A
is called a subclass of the class B
because it has all the properties of the class B
.
So in the above example, Student
is a subtype of Person
since it contains everything that Person
has and perhaps more. However, structural subtyping is not legal in all the scenarios. Let’s see the following example.
TypeScript allows using a variable reference for substituting a subtype but complains where literal value is used. This behavior perhaps could have been implemented to avoid misleading behavior. As you can clearly see, all errors have occurred where a deliberate attempt was made.
You can read more about type compatibility from this documentation.
any vs unknown type
In the previous lesson, we learned that the type guards help the TypeScript compiler narrow down a type from a set of possibilities (union) to a definite type based on a condition that is unique to the type.
In most cases, you do not need to provide a type to an entity since it will be inferred from the initial value of that entity such as the default value of a function parameter. The same goes for the return value of a function or initial value of an object’s properties or others.
When TypeScript can’t decide which type is suitable for a variable, it implicitly set the type of that variable to any
. For example, let x;
expression declares a variable x
but since we haven’t provided a type to it or given an initial value, the default type of variable x
is set to any
.
The any
types only exist in TypeScript and it acts as a collective type for all the values that can exist in JavaScript runtime. That means, string
, number
, symbol
, null
, undefined
and all other possible values that exist in JavaScript are also a type of any
. Hence any
type is sometimes called a top type or a supertype.
// type Collection = any
type Collection = string | number | undefined | any;
// type Collection = any
type Collection = string & any;
Since any
is a supertype of all the JavaScript values, a union type that contains any
will be narrowed to any
as displayed above. Since intersection generates a type by combining the shape of two types, an intersection of any type with any
returns any
.
This means you can declare a variable x
of type any
and assign any JavaScript value to it, again and again, and the TypeScript compiler won’t complain about it. TypeScript will go one step further and let you assign a value of type any
to a variable of known type. This is visualized below.
let x: any;
x = 1;
x = "one";
x = true;
let y: boolean = x; // violates subtype principle :(
This creates a big problem at runtime. Since a value of type any
doesn’t have a definite shape, TypeScript will let you write a program that can use this value in whichever way possible. For example, it will let you call it like a function or instantiate it like a class. TypeScript puts all the faith in you when it comes to any
. Hence a program with the type any
might not work as expected or leave to disastrous results at runtime.
In the above program, since func
argument of the calculate
function has the type of any
, you can to pass undefined
as a legal value for it. As you can see, this program won’t work with undefined
value since it’s not a function.
There are endless scenarios where any
type can cause havoc. For example, if you expect a value x
of type any
to be an object at runtime, you can access a property on it such as x.a.b
and TypeScript will allow it. But if x
is not an object at runtime, this expression will result in an error.
Since any
type doesn’t have a shape, you won’t be getting any help from your IDE for autocompletion and IntelliSense. You can, however, use type assertion syntax such as x as Person
to assert type x
of type any
to Person
.
We can also use type guard with any
to narrow the type. Since any
represents a collection of many types, just like a union, TypeScript can discriminate a type using a type guard.
In the above example, the person
argument of the function getPosition
is any
. The instanceof
type guard narrows the person
type from any to Student
in the first if
block and Player
in the second if
block.
If you notice, we had to use else if
block instead of just else
. This is because TypeScript can’t magically understand that the else
block contains the Player
type. We are dealing with any
type here and any
can represent any values, and not just Student
and Player
. Don’t confuse it with unions.
The any
type is very useful when you have no idea what shape a value might take at runtime. This generally happens when you are using third party API and TypeScript can’t figure out the types. To silence the TypeScript compilation errors, you’re compelled to use any
type.
However, TypeScript introduced unknown
type in TypeScript 3.0 to mitigate some of the problems associated with any
. Basically, unknown
is a type that tells the TypeScript compiler than the type of a value is unknown at the compile-time but it can take any shape at runtime.
Hence, unknown
represents the same values that any
can represent making it another top type or supertype in TypeScript. Similar to any
, a union type that contains unknown
will be narrowed to unknown
, but any
get’s a preference over unknown
as shown below.
// type Collection = unknown
type Collection = string | number | undefined | unknown;
// type Collection = any
type Collection = string | number | undefined | unknown | any;
// type Collection = string
type Collection = string & unknown;
However, the unknown
type behaves differently when it comes to the type intersection. An intersection of any type with the unknown
returns the same type. Since intersection creates a new type by combining shapes of two types, unknown
doesn’t represent any shape, hence this behavior.
You can store pretty much any JavaScript value in a variable of type unknown
including the value of type any
. However, you won’t be able to store a value of type unknown
in a variable of known type.
let x: unknown;
x = 1;
x = "one";
x = true;
// Error: Type 'unknown' is not assignable to type 'boolean'.
let y: boolean = x;
This happens because y
can only contain the a value of type boolean
and a value of type unknown
can take any shape at runtime. You might argue the same with any
, but that’s why unknown
was introduced.
TypeScript also won’t allow you to perform operations on a value of type unknown
unless a type is narrowed using type assertion or a type guard.
let x: unknown;
// Error: Property 'a' does not exist on type 'unknown'.
console.log(x.a);
// Error: This expression is not callable.
x();
// Error: This expression is not constructable.
new x();
Comparison with void and never
From the Basic Types lesson, we’ve learned that the void
type represents the return value of a function that doesn’t have a return statement. Similarly, the never
type represents the return type of a function that will never return any value. These types do not represent any values at runtime and these act as an aid for TypeScript’s type system.
Due to this fact, void
and never
do not present any values that any
and unknown
represents. However, in a union type containing void
and never
along with any
or unknown
, the union will be narrowed to any
or unknown
.
// type Collection = any
type Collection = void | never | any;
// type Collection = unknown
type Collection = void | never | unknown;
typeof Keyword
If your objects need to have a definite shape and you want to enforce that rule, using an interface is an efficient and secure option. However, we didn’t go the other way around. What if we want to create an interface from the shape of the object?
Well, you can do that using typeof
keyword. This keyword acts differently when used in type annotation syntax. When typeof
keyword is followed by an object, it returns the shape of that object as an interface.
In the above example, we have defined the ross
object which has firstName
and lastName
properties. Since both these properties have string
values, the TypeScript compiler creates an interface implicitly that looks like below and ross
variable gets that type.
{
firstName: string;
lastName: string;
}
The typeof ross
expression may return 'object'
at runtime but when it is used in type annotation like let x: typeof ross
, it returns the implicit interface type associated with the ross
variable. Hence we can use this expression to enforce the type of ross
on other values.
We can also create an alias from like we created Person
type in the above example. This is just not restricted to objects but you can use typeof
keyword to extract the type of any value, such as string
or interface
.
declare Keyword
While writing a frontend JavaScript application, you use third-party libraries that are available only at runtime. For example, when you import lodash from a CDN, it injects _
variable in the window
object to give access to the library APIs such as _.[tail](https://lodash.com/docs/4.17.15#tail)
function.
The same goes for the native APIs provided by the device or browser that are only available at runtime. When you write a TypeScript program that uses one of such library functions, the TypeScript compiler complains that such a function doesn’t exist.
// Error: Cannot find name '_'
const result = _.tail([1, 2, 3]);
We can’t blame the TypeScript compiler for that since we are trying to access a value that isn’t defined in the program. The obvious choice is to define such a value and then use it, but it will just create a new value in the current scope.
var _: any;
In the above program, we have defined the _
variable and if we access any functions from this object, the TypeScript compiler won’t complain because first, this value exists and second, its type is any
.
However, at runtime, we just completely bypassed the _
global variable provided by the library and we have a _
variable in the current scope that is probably undefined
. This isn’t quite what we wanted.
This is where declare
keyword comes into play. If we put declare
keyword before a variable declaration, it won’t create a new variable in the scope, however, it will tell the TypeScript compiler that this value exists at runtime and it has the provided type (shape).
In the above program, we have defined an interface Lodash
that contains the signatures of the functions exposed by the lodash
library during the runtime. Then we declared a constant _
of type Lodash
which will instruct the TypeScript compiler that _
value exists during the runtime and it has the shape of Lodash
interface. This program will compile just fine.
💡 If you are familiar with
C
orC++
programming languages, thedeclare
keyword is similar to theextern
keyword which tells the compiler to assume the definition of an entity (such as a function) is available at runtime.
The declarations of the external values using declare
keyword along with their types are called ambient declarations. Normally these declarations are stored in files ending width d.ts
extensions (AKA type definition files). These are normal TypeScript files and are they are imported automatically by TypeScript compiler implicitly or with the help of tsconfig.json
.
TypeScript comes with a lot of ambient declarations. If you check lib
directory of the standard TypeScript installation, it has type definition files for Web APIs such as DOM
, fetch
, WebWorker
as well as type definitions for JavaScript language features. These are imported automatically.
💡 We have discussed ambient declarations, type declaration files, standard library, and how to working with external libraries in the Declaration Files lesson (coming soon).
Script vs Module
TypeScript treats source (.ts
) files differently based on what their role is. A JavaScript file can act as a module or as a script.
What is a script?
A script file is imported using traditional <script>
tag in the browser. All the values (such as a variable or a function) defined in the script file (on the file level) are available in the global scope (also called the **global namespace), hence you can access these values from another script file.
// main.js
var ross = { firstName: "Ross", lastName: "Geller" };
var fullname = sayHello(ross); // 'Hello, Ross'
// vendor.js
var prefix = "Hello, ";
function sayHello(person) {
var result = prefix + person.firstName;
return result;
}
In the above example, main.js
and vendor.js
are normal JavaScript files. The main.js
expects the sayHello
function to be available at runtime. This is provided by the vendor.js
. Hence the order of their loading is important.
<script src="./vendor.js"></script>
<script src="./main.js"></script>
All the values declared in script files at the file level are available in the global scope. Hence variable prefix
and function sayHello
from the vendor.js
is added to the global scope and becomes available in the main.js
. Similarly, the variable ross
and name
from the main.js
becomes available in vendor.js
. That’s not the case with the variable result
since it is defined in a function so it is restricted to the function scope only.
💡 All the values from the global scope are accessible on the
window
in when the program is running inside a browser (such aswindow.sayHello()
).
When you open two or more script files in VSCode, you immediately start to see errors when the variables/constants with the same name are defined in opened files. This is because VSCode assumes that these files will be imported inside a browser and it tries to warn about global scope having the same value declarations repeated across many files.
What is a module?
A module is a sandboxed script file where values declared on the file level do not pollute the global scope unless explicitly specified. All these values are restricted to the file itself and they are not visible outside the file.
Since a module doesn’t expose values, a module can’t also see the values defined in other modules. The only way to share values between modules is by using import
and export
keyword.
The export
keyword makes a value available for import but doesn’t add it to the global scope. So another module needs to use the import
keyword to import this value. I have explained how the module works in the Module System (coming soon) lesson so let’s dive directly into an example.
// main.js
import { sayHello } from "./vendor";
export var ross = { firstName: "Ross", lastName: "Geller" };
var fullname = sayHello(ross); // 'Hello, Ross'
// vendor.js
var prefix = "Hello, ";
export function sayHello(person) {
var result = prefix + person.firstName;
return result;
}
In the above case, both the vendor.js
and main.js
are modules since they use import
and export
statements. In this case, the vendor.js
can’t see the variable ross
since it is not imported in the vendor.js
and similarly, variable prefix
is not available in the main.js
since it is not exported from vendor.js
in the first place. This is an implicit abstraction provided by modules that prevent accidental pollution of the global scope.
As we can see, a module can’t pollute the global scope in this case. The only way to add some value to the global scope is by using the window
object in the case of browser or the cross-platform globalThis
object.
// main.js
import { sayHello } from "./vendor";
export var ross = { firstName: "Ross", lastName: "Geller" };
var fullname = sayHello(ross); // 'Hello, Ross'
console.log(window.prefix); // 'Hello, '
// vendor.js
var prefix = "Hello, ";
window.prefix = prefix;
export function sayHello(person) {
var result = prefix + person.firstName;
return result;
}
In the above case, prefix
will be available in the main.js
through window.prefix
since the vendor.js
has explicitly added prefix
to the global scope (window
).
How does this affect TypeScript?
Well, TypeScript is a superset of JavaScript, so it has to support all the principles of JavaScript. So this behavior is expected to be the same in the TypeScript as well. However, this behavior applies to the types as well.
Let’s first create a project that contains main.ts
and vendor.ts
file. First, we will treat main.ts
and vendor.ts
as script files, so they won’t contain an import
or an export
statement. The simplest form of the tsconfig.json
file to compile this project looks like the following. You can learn more about this file from the Compilation (coming soon) lesson.
{
"files": ["./main.ts", "./vendor.ts"],
"compilerOptions": {
"outDir": "dist"
}
}
In the above example, function sayHello
from the vendor.ts
is accessible in the main.ts
and the type Person
interface from the main.ts
is accessible in the vendor.ts
. Since these will be compiled as script files (due to lack of import
or export
statements), TypeScript assumes these files will be loaded in the browser using traditional <script>
tag.
See what happens as soon as we add import
or export
statement in the file.
By merely adding the export {}
statement (which does nothing BTW), TypeScript assumes that these will be module files. Hence, the main.ts
can’t access sayHello
from the vendor.ts
as explained below.
If you inspect carefully, the Person
interface defined in the main.ts
is also not accessible in the vendor.ts
since main.ts
is a module. So if you want to share values as well as types between modules, you need to explicitly export
and import
them.
In the above case, the sayHello
function is imported inside the main.ts
and the type Person
interface is imported inside vendor.ts
.
In the above example, we tried to forcefully share the prefix
variable between the vendor.ts
and the main.ts
but the attempt failed. When we compiled the program, the TypeScript compiler throws the following error.
Property 'prefix' does not exist on type 'Window & typeof globalThis'.
This happens because window
has the type of Window
interface (provided by the TypeScript’s standard library) and it doesn’t contain the declaration of prefix
property. So we would need to manually add it.
In the above example, we have explicitly mentioned that the prefix
property is available in the global scope using the declare global
statement. We will learn more about the declare global
statement and type declarations in the Declaration Files (coming soon) lesson.
💡 Above example creates a cyclic dependency between
main.js
andvendor.js
so do not implement this example in the production unless you are using a module loader that handles these kinds of situations.