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:
tsTrylettemperature = 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.
tsTrylettemperature = 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:
tsTryconsthumidity = 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:
constvariable declarations cannot be reassigned- the initial value assigned to
humidityis 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
tsTrylettemperature = 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.
tsTrylettemperature = 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
tsTrylettemperature = 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.
tsTrytemperature = 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.
tsTrylettemp2 = 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.
tsTryletfrontEndMastersFounding = 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
tsTryconsthumidity = 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.
tsTryletdate3 = "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
tsTryletConversion 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.
tsTryfunctionadd (a ,b ) {returna +b // strings? numbers? a mix?}
Here’s what your in-editor tooltip would look like if you were using this function:
tsTryconstresult =add (3, "4")result
Without type annotations, “anything goes” for the arguments passed into add. Why is this a problem?
tsTryconstresult =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:
tsTryfunctionadd (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:
tsTryfunctionadd (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
tsTryfunctionA 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.
tsTryfunctionadd (a : number,b : number): number {returna +b }