Now that we have compiled a simple TypeScript program, let’s look at the basics of the programming language.
Variable Declarations & Inference
Since 2015, the conventional way to declare JavaScript variables is with let
and
const
like this:
tsTry
lettemperature = 19
As we can see, TypeScript is able to infer
that temperature
is a number, based on the fact that we’re initializing it with
a value as we are declaring it.
If we try to give temperature
a value that is incompatible with the number
it was initially used to hold, we’ll get an error.
tsTry
lettemperature = 6Type 'string' is not assignable to type 'number'.2322Type 'string' is not assignable to type 'number'.= "warm" temperature
In TypeScript, variables are “born” with their types. Although
there are ways of making them more specific in certain branches of code,
there’s no way to change temperature
’s type from number
to string
without telling
typescript to disregard all of the type information on this variable.
Let’s try the same thing with const
:
tsTry
consthumidity = 79
Notice that the type of this variable is not number
, it’s 79
. TS is able to
make a more specific assumption here, because:
const
variable declarations cannot be reassigned- the initial value assigned to
humidity
is a number, which is an immutable value type
Therefore humidity
will always be 79
in this program.
Literal Types
Types like 79
are called a literal types — you can think of this as
“only 79
is allowed”
There’s a common idea you’ll see again and again when working with TypeScript.
Inference is not so specific as to get in the way of common behavior.
For example, the let
variable declaration above could have assumed age
to
be of type 79
, but this would have interfered with our ability to set this
re-assignable variable to 7
or 8
.
A type as a set of allowed values
It’s often useful to think about a type as representing some group of allowed values. We’ll use a common syntax for describing these sets that looks like this:
js
{ 1, 2, 3 } // "1 or 2 or 3"
Let’s look at our examples from above
tsTry
lettemperature = 19consthumidity = 79
The number
type of temperature
represents the set { all possible numbers }
.
You can assign a new number to temperature
and TypeScript will be perfectly happy to allow it.
tsTry
lettemperature = 19temperature = 23
The 79
type of humidity
represents the set { 6 }
, meaning
“any value, as long as it’s a 6
“.
We can create an interesting situation by forcing a let
variable declaration
to have its type inferred as if it’s a const
tsTry
lettemperature = 19;lethumidity = 79 asconst ;
Note that we have the same types as before — the only thing is changed is we have re-assignability. Let’s continue below and try some assignments.
tsTry
temperature = 23; // (1) OK, as beforetemperature =humidity ; // (2) OKType 'number' is not assignable to type '79'.2322Type 'number' is not assignable to type '79'.= humidity temperature ; // (3) ❌ ERRORhumidity = 79; // (4) OKType '78' is not assignable to type '79'.2322Type '78' is not assignable to type '79'.= 78; // (5) ❌ ERROR humidity
Each of these x = y
assignments involves making some determination of type equivalence, which
means asking the question “does the type of y
fit within the type of x
?.
Let’s describe what’s happening here using sets.
tsTry
lettemp2 = 19; // temp2's type is { all numbers }lethumid2 = 79 asconst ; // humid2's type is { 79 }////// Is each member in { 23 } also in { all numbers }? ✅ YEStemp2 = 23;// Is each member in { 79 } also in { all numbers }? ✅ YEStemp2 =humid2 ;// Is each member in { all numbers } also in { 79 }? ❌ NOType 'number' is not assignable to type '79'.2322Type 'number' is not assignable to type '79'.= humid2 temp2 ;// Is each member in { 79 } also in { 79 } ✅ YEShumid2 = 79;// Is each member in { 78 } also in { 79 } ❌ NOType '78' is not assignable to type '79'.2322Type '78' is not assignable to type '79'.= 78; humid2
What we can see is that the type 79
is type-equivalent to number
, but not
the other way around. { 79 }
is a subset of { all numbers }
and thus
the type 79
is a subtype of number
.
Implicit any
and type annotations
Sometimes, we need to declare a variable before it gets initialized, like
endTime
below:
tsTry
// between 500 and 1000constRANDOM_WAIT_TIME =Math .round (Math .random () * 500) + 500letstartTime = newDate ()letendTime setTimeout (() => {endTime = 0endTime = newDate ()},RANDOM_WAIT_TIME )
endTime
is “born” without a type, so it ends up being an implicit any
.
Think of any
as “the normal way JS variables work”, in that you could assign
endTime
a number
, then later a function
, then a string
.
TypeScript doesn’t have enough information around the declaration site to infer
what endTime
should be, so it gets the most flexible type: any
. Going
back to our comparison of types to sets, any
represents
the set { all possible values }
.
If we wanted more safety here, we could add a type annotation:
diff
- let endTime+ let endTime: Date
tsTry
// between 500 and 1000constRANDOM_WAIT_TIME =Math .round (Math .random () * 500) + 500letstartTime = newDate ()letendTime :Date setTimeout (() => {Type 'number' is not assignable to type 'Date'.2322Type 'number' is not assignable to type 'Date'.= 0 endTime endTime = newDate ()},RANDOM_WAIT_TIME )
Now, TypeScript will correctly alert us when we try to flip flop between the
number 0
and a Date
.
Type Casting
There may be occasions, especially when exploring TypeScript where we want to force the compiler to regard a value as being of a particular type. This is called type casting.
tsTry
letfrontEndMastersFounding = newDate ("Jan 1, 2012")letdate1 =frontEndMastersFounding letdate2 =frontEndMastersFounding as any; // force the type to be `any`
This is something that you should do very carefully. It’s sometimes safe to cast to a more general type, but potentially dangerous to cast to a more specific or unrelated type.
Here’s an example of a safe (but rather pointless) cast
tsTry
consthumidity = 79 as number; // is 79 a number? If so, this is safe!
and here’s an example of an unsafe cast. This kind of pattern effectively makes TypeScript lie to you.
tsTry
letdate3 = "oops" as any asDate date3 // TypeScript thinks this is a Date now, but it's really a stringdate3 .toISOString () // what do we think will happen when we run this? 💥
note that in the above example, we first have to cast up to any
, and then back down to Date
.
TypeScript doesn’t even allow us to cast directly from string
to Date
because it’s dangerous
tsTry
letConversion of type 'string' to type 'Date' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.2352Conversion of type 'string' to type 'Date' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.date4 = "oops" asDate
Function arguments and return values
The : Date
syntax we’ve just seen for variable type annotations can also be used
to describe function arguments and return values. In this example it’s not clear,
even from the implementation of the function, whether add
should accept numbers
or strings.
tsTry
functionadd (a ,b ) {returna +b // strings? numbers? a mix?}
Here’s what your in-editor tooltip would look like if you were using this function:
tsTry
constresult =add (3, "4")result
Without type annotations, “anything goes” for the arguments passed into add
. Why is this a problem?
tsTry
constresult =add (3, "4")constp = newPromise (result )
If you’ve ever created a Promise
using the promise constructor, you may see
that we are using a string
where we should use a two-argument function. This
is the kind of thing we’d hope that TypeScript could catch for us.
Let’s add some type annotations to our function’s arguments:
tsTry
functionadd (a : number,b : number) {returna +b }constArgument of type 'string' is not assignable to parameter of type 'number'.2345Argument of type 'string' is not assignable to parameter of type 'number'.result =add (3,"4" )
Great, now we can enforce that only values of type number
are passed into the function,
and TS can now determine the return type automatically:
tsTry
functionadd (a : number,b : number) {returna +b }constresult =add (3, 4)
If we wanted to specifically state a return type, we could do so using basically the same syntax in one more place
tsTry
functionA function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.2355A function whose declared type is neither 'undefined', 'void', nor 'any' must return a value.add (a : number,b : number):number {}
This is a great way for code authors to state their intentions up-front. TypeScript will make sure that we live up to this intention, and errors will be surfaced at the location of the function declaration instead of where we use the value returned by the function. Once we implement the body of the function, we’ll no longer see this error.
tsTry
functionadd (a : number,b : number): number {returna +b }