TypeScript 3.0 is out! It comes with enhancements for the type system, compiler, and language service. This release is shipping with the following:
Project references let TypeScript projects depend on other TypeScript projects by allowing
tsconfig.json
files to reference othertsconfig.json
files.Support for
defaultProps
in JSX.Tuples in rest parameters and spread expressions with optional elements.
New
unknown
top type! It's the type-safe counterpart ofany
.
For this blog post, we are going to focus on the enhancements made to tuples and the unknown
type! Feel free to check the handbook for an in-depth view of TypeScript Project References.
I'm getting more and more intrigued by TypeScript. Curious if you're: (poll)
— Sarah Drasner (@sarahedo) <a href="https://twitter.com/sarahedo/status/986305983742791681?ref_src=twsrc%5Etfw">April 17, 2018</a>
TypeScript Quick Setup
If you want to follow along with the example in this post, you can follow these quick steps to get a TypeScript 3.0 project up and running.
If you prefer to test TypeScript 3.0 on a sandbox environment, you can use the TypeScript playground instead to follow along.
Setting Up a TypeScript Project
In any directory of your choice, create a ts3
directory and make it your current directory:
mkdir ts3
cd ts3
Once ts3
is the current working directory, initialize a Node.js project with default values:
npm init -y
Next, install core packages that are needed to compile and monitor changes in TypeScript files:
npm i typescript nodemon ts-node --save-dev
A TypeScript project needs a tsconfig.json
file. This can be done in two ways: using the global tsc
command or using npx tsc
. I recommend using npx
.
The npx
command is available in npm >= 5.2
and it lets you create a tsconfig.json
file as follows:
npx tsc --init
Here, npx
executes the local typescript
package that has been installed locally.
If you prefer to do so using a global package, ensure that you install TypeScript globally and run the following:
tsc --init
You will see the following message in the command line once's that done:
Successfully created a tsconfig.json file.
You will also have a tsconfig.json
file with sensible started defaults enabled. For the scope of this tutorial, those configuration settings are more than enough.
Compiling, Running, and Watching TypeScript
In a development world where everything build-related is now automated, an easy way to compile, run, and watch TypeScript files is needed. This can be done through nodemon
and ts-node
:
nodemon
: It's a tool that monitors for any changes in a Node.js application directory and automatically restarts the server.ts-node
: It's a TypeScript execution and REPL for Node.js, with source map support. Works withtypescript@>=2.0
.
The executables of these two packages need to be run together through an npm
script. In your code editor or IDE, update the package.json
as follows:
{
// ...
"scripts": {
"watch": "nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts"
}
// ...
}
The watch
script is doing a lot of hard work:
nodemon --watch 'src/**/*.ts' --exec 'ts-node' src/index.ts
The nodemon
executable takes a --watch
argument. The --watch
option is followed by a string that specifies the directories that need to be watched and follows the glob pattern>).
Next, the --exec
option is passed. The --exec
option is used to run non-node scripts. nodemon
will read the file extension of the script being run and monitor that extension instead of .js
. In this case, --exec
runs ts-node
.
ts-node
executes any passed TypeScript files as node
+ tsc
. In this case, it receives and executes src/index.ts
.
In some other setups, two different shells may be used: One to compile and watch the TypeScript files and another one to run resultant JavaScript file through
node
ornodemon
.
Finally, create a src
folder under the project directory, ts3
, and create index.ts
within it.
Running a TypeScript Project
With all dependencies installed and scripts set up, you are ready to run the project. Execute the following command in the shell:
npm run watch
Messages about nodemon
will come up. Since index.ts
is empty as of now, there's no other output in the shell.
You are all set up! Now join me in exploring what new features come with TypeScript 3.
TypeScript TupleWare
What Are TypeScript Tuples?
TypeScript 3 comes with a couple of changes to how tuples can be used. Therefore, let's quickly review the basics of TypeScript tuples.
A tuple is a TypeScript type that works like an array with some special considerations:
- The number of elements of the array is fixed.
- The type of the elements is known.
- The type of the elements of the array need not be the same.
For example, through a tuple, we can represent a value as a pair of a string and a boolean. Let's head to index.ts
and populate it with the following code:
// Declare the tuple
let option: [string, boolean];
// Correctly initialize it
option = ["uppercase", true];
If we change value of option
to [true, "uppercase"]
, we'll get an error:
// Declare the tuple
let option: [string, boolean];
// Correctly initialize it
option = ["uppercase", true];
// Incorrect value order
option = [true, "uppercase"];
src/index.ts(8,11): error TS2322: Type 'true' is not assignable to type 'string'.
src/index.ts(8,17): error TS2322: Type 'string' is not assignable to type 'boolean'.
Let's delete the incorrect example from our code and move forward with the understanding that, with tuples, the order of values is critical. Relying on order can make code difficult to read, maintain, and use. For that reason, it's a good idea to use tuples with data that is related to each other in a sequential manner. That way, accessing the elements in order is part of a predictable and expected pattern.
The coordinates of a point are examples of data that is sequential. A three-dimensional coordinate always comes in a three-number pattern:
(x, y, z)
On a Cartesian plane, the order of the points will always be sequential. We could represent this three-dimensional coordinate as the following tuple:
type Point3D = [number, number, number];
Therefore, Point3D[0]
, Point3D[1]
, and Point3D[2]
would be logically digestible and easier to map than other disjointed data.
On the other hand, associated data that is loosely tied is not beneficial. For example, we could have three pieces of customerData
that are email
, phoneNumber
, and dateOfBirth
. customerData[0]
, customerData[1]
, and customerData[2]
say nothing about what type of data each represents. We would need to trace the code or read the documentation to find out how the data is being mapped. This is not an ideal scenario and using an interface
would be much better.
That's it for tuples! They provide us with a fixed size container that can store values of all kinds of types. Now, let's see what TypeScript changes about using tuples in version 3.0
of the language.
Using TypeScript Tuples in Rest Parameters
In JavaScript, the rest parameter syntax allows us to "represent an indefinite number of arguments as an array."
However, as we reviewed earlier, in TypeScript, tuples are special arrays that can contain elements of different types.
With TypeScript 3, the rest parameter syntax allows us to represent a finite number of arguments of different types as a tuple. The rest parameter expands the elements of the tuple into discrete parameters.
Let's look at the following function signature as an example:
declare function example(...args: [string, boolean, number]): void;
Here, the args
parameter is a tuple type that contains three elements of type string
, boolean
, and number
. Using the rest parameter syntax, (...
), args
is expanded in a way that makes the function signature above equivalent to this one:
declare function example(args_0: string, args_1: boolean, args_2: number): void;
To access args_0
, args_1
, and args_2
within the body of a function, we would use array notation: args[0]
, args[1]
, and args[2]
.
The goal of the rest parameter syntax is to collect "argument overflow" into a single structure: an array or a tuple.
In action, we could call the example
function as follows:
example("TypeScript example", true, 100);
TypeScript will pack that into the following tuple since example
, as first defined, only takes one parameter:
["TypeScript example", true, 100];
Then the rest parameter syntax unpacks it within the parameter list and makes it easily accessible to the body of the function using array index notation with the same name of the rest parameter as the name of the tuple, args[0]
.
Using our Point3D
tuple example, we could define a function like this:
declare function draw(...point3D: [number, number, number]): void;
As before, we would access each coordinate point as follows: point3D[0]
for x
, point3D[1]
for y
, and point3D[2]
for z
.
How is this different from just passing an array? A tuple type forces us to pass a number for x
, y
, and z
while an array could be empty. A tuple will throw an error if we are not passing exactly 3 numbers to the draw
function.
Spread Expressions with TypeScript Tuples
The rest parameter syntax looks very familiar to the spread operator; however, they are very different. As we learned earlier, the rest parameter syntax collects parameters into a single variable and then expands them under its variable name. On the other hand, the spread operator expands the elements of an array or object. With TypeScript 3.0, the spread operator can also expand the elements of a tuple.
Let's see this in action using our Point3D
tuple. Let's replace the code in index.ts
and populate it with the following:
type Point3D = [number, number, number];
const draw = (...point3D: Point3D) => {
console.log(point3D);
};
const xyzCoordinate: Point3D = [10, 20, 10];
draw(10, 20, 10);
draw(xyzCoordinate[0], xyzCoordinate[1], xyzCoordinate[2]);
draw(...xyzCoordinate);
We create a Point3D
to represent the (10, 20, 10)
coordinate and store it in the xyzCoordinate
constant. Notice that we have three ways to pass a point to the draw
function:
- Passing the values as literals:
draw(10, 20, 10);
- Passing indexes to the corresponding
xyzCoordinate
tuple:
draw(xyzCoordinate[0], xyzCoordinate[1], xyzCoordinate[2]);
- Using the spread operator to pass the full
xyzCoordinate
tuple:
draw(...xyzCoordinate);
As we can see, using the spread operator is a fast and clean option for passing a tuple as an argument.
Optional Tuple Elements
With TypeScript 3.0, our tuples can have optional elements that are specified using the ?
postfix.
We could create a generic Point
tuple that could become two or three-dimensional depending on how many tuple elements are specified:
type Point = [number, number?, number?];
const x: Point = [10];
const xy: Point = [10, 20];
const xyz: Point = [10, 20, 10];
We could determine what kind of point is being represented by the constant by checking the length
of the tuple. Let's replace the content of index.ts
with the following:
type Point = [number, number?, number?];
const x: Point = [10];
const xy: Point = [10, 20];
const xyz: Point = [10, 20, 10];
console.log(x.length);
console.log(xy.length);
console.log(xyz.length);
The code above outputs the following in the console:
1
2
3
Notice that the length of each of the Point
tuples varies by the number of elements the tuple has. The length
property of a tuple with optional elements is the "union of the numeric literal types representing the possible lengths" as stated in the TypeScript release.
The type of the length property in the Point
tuple type [number, number?, number?]
is 1 | 2 | 3
.
As a rule, if we need to list multiple optional elements, they have to be listed with the postfix ?
modifier on its type at the end of the tuple. An optional element cannot have required elements to its right but it can have as many optional elements as desired instead.
The following code would result in an error:
type Point = [number, number?, number];
error TS1257: A required element cannot follow an optional element.
In --strictNullChecks
mode, using the ?
modifier automatically includes undefined
in the tuple element type. This behavior is similar to what TypeScript does with optional parameters.
Tuples are like Tupperware containers that let you store different types of food on each container. We can collect and bundle the containers and we can disperse them as well. At the same time, we can make it optional to fill a container. However, it would be a good idea to keep the containers with uncertain content at the bottom of the stack to make the containers with guaranteed content both easy to find and predictable.
Rest Elements In Tuple Types
In TypeScript 3.0, the last element of a tuple can be a rest element, ...element
. The one restriction for this rest element is that it has to be an array type. This structure is useful when we want to create open-ended tuples that may have zero or more additional elements. It's a boost over the optional ?
postfix as it allows us to specify a variable number of optional elements with the one condition that they have to be of the same type.
For example, [string, ...number[]]
represents a tuple with a string element followed by any amount of number
elements. This tuple could represent a student name followed by test scores.
Replace the content of index.ts
with the following:
type TestScores = [string, ...number[]];
const thaliaTestScore = ["Thalia", ...[100, 98, 99, 100]];
const davidTestScore = ["David", ...[100, 98, 100]];
console.log(thaliaTestScore);
console.log(davidTestScore);
Output:
[ 'Thalia', 100, 98, 99, 100 ]
[ 'David', 100, 98, 100 ]
Don't forget to add the
...
operator to thearray
. Otherwise, the tuple will get the array as it second element and nothing else. Thearray
elements won't spread out into the tuple:
type TestScores = [string, ...number[]];
const thaliaTestScore = ["Thalia", [100, 98, 99, 100]];
const davidTestScore = ["David", [100, 98, 100]];
console.log(thaliaTestScore);
console.log(davidTestScore);
Output:
[ 'Thalia', [ 100, 98, 99, 100 ] ]
[ 'David', [ 100, 98, 100 ] ]
TypeScript: New 'Unknown' Top Type
TypeScript 3.0 introduces a new type called unknown
. unknown
acts like a type-safe version of any
by requiring us to perform some type of checking before we can use the value of the unknown
element or any of its properties. Let's explore the rules around this wicked type!
any
is too flexible. As the name suggests, it can encompass the type of every possible value in TypeScript. What's not so ideal of this premise is that any
doesn't require us to do any kind of checking before we make use of the properties of its value.
In the following code, itemLocation
is defined as having type any
and it's assigned the value of 10
, but we use it unsafely. Replace the content of index.ts
with this:
let itemLocation: any = 10;
itemLocation.coordinates.x;
itemLocation.coordinates.y;
itemLocation.coordinates.z;
const findItem = (loc: string) => {
console.log(loc.toLowerCase());
};
findItem(itemLocation);
itemLocation();
const iPhoneLoc = new itemLocation();
We tried to access a coordinates
property from itemLocation
that doesn't exist. We also passed it as an argument to a function that takes an argument of type string
. We called it as a function. Lastly, we used it as a constructor. Throughout the execution of these statements, TypeScript throws errors but it doesn't get upset enough to stop compilation:
TypeError: Cannot read property 'x' of undefined
Let's see what happens when we change the type of itemLocation
from any
to unknown
:
let itemLocation: unknown = 10;
itemLocation.coordinates.x;
itemLocation.coordinates.y;
itemLocation.coordinates.z;
const findItem = (loc: string) => {
console.log(loc.toLowerCase());
};
findItem(itemLocation);
itemLocation();
const iPhoneLoc = new itemLocation();
Our IDE or code editor will instantly underline itemLocation
with red in any of the instances where we are accessing its properties unsafely. This time around, TypeScript doesn't merely throw an error but actually stops compilation:
TSError: ⨯ Unable to compile TypeScript:
src/index.ts(3,1): error TS2571: Object is of type 'unknown'.
src/index.ts(4,1): error TS2571: Object is of type 'unknown'.
src/index.ts(5,1): error TS2571: Object is of type 'unknown'.
src/index.ts(11,10): error TS2345: Argument of type 'unknown' is not assignable to parameter of type 'string'.
src/index.ts(13,1): error TS2571: Object is of type 'unknown'.
src/index.ts(15,23): error TS2571: Object is of type 'unknown'.
We can use an unknown
type if and only if we perform some form of checking on its structure. We can either check the structure of the element we want to use, or we can use type assertion to tell TypeScript that we are confident about the type of the value. Let's see this in code.
Performing an Explicit Structural Check
Let's start with the following code that uses any
as the type of itemLocation
:
let itemLocation: any = {
coordinates: { x: 10, y: "cows", z: true }
};
console.log(itemLocation.coordinates.x);
console.log(itemLocation.coordinates.y);
console.log(itemLocation.coordinates.z);
This code is valid and the output in the console is as follows:
10
cows
true
However, if we change the type to unknown
, we immediately get a compilation error:
let itemLocation: unknown = {
coordinates: { x: 10, y: "cows", z: true }
};
console.log(itemLocation.coordinates.x);
console.log(itemLocation.coordinates.y);
console.log(itemLocation.coordinates.z);
return new TSError(diagnosticText, diagnosticCodes)
^
TSError: ⨯ Unable to compile TypeScript:
src/index.ts(5,13): error TS2571: Object is of type 'unknown'.
src/index.ts(6,13): error TS2571: Object is of type 'unknown'.
src/index.ts(7,13): error TS2571: Object is of type 'unknown'.
Because itemLocation
is of type unknown
, despite itemLocation
having the coordinates
property object defined with its own x
, y
, and z
properties, TypeScript won't let the compilation happen until we perform a type-check.
Let's create an itemLocationCheck
method that checks for the structural integrity of itemLocation
:
let itemLocation: unknown = {
coordinates: { x: 10, y: "cows", z: true }
};
const itemLocationCheck = (
loc: any
): loc is { coordinates: { x: any; y: any; z: any } } => {
return (
!!loc &&
typeof loc === "object" &&
"coordinates" in loc &&
"x" in loc.coordinates &&
"y" in loc.coordinates &&
"z" in loc.coordinates
);
};
if (itemLocationCheck(itemLocation)) {
console.log(itemLocation.coordinates.x);
console.log(itemLocation.coordinates.y);
console.log(itemLocation.coordinates.z);
}
We get the output back in the console:
10
cows
true
When we pass itemLocation
to itemLocationCheck
as a parameter, itemLocationCheck
performs a structural check on it:
- It checks that
loc
is defined. - It checks that
loc
is of typeobject
. - It checks that
loc
has a property namedcoordinates
.- Once this is done, it checks that
coordinates
has properties namedx
,y
, andz
.
- Once this is done, it checks that
If all these checks pass, the logic within the if
statement is executed. These checks are enough to convince TypeScript that we have done our due diligence on checking the integrity of the unknown
type element and it gives us permission to use it.
How strict is TypeScript with the structural check? Do we need to check for the existence of every property of the object? Let's modify itemLocationCheck
for it not to check the coordinates.z
property:
let itemLocation: unknown = {
coordinates: { x: 10, y: "cows", z: true }
};
const itemLocationCheck = (
loc: any
): loc is { coordinates: { x: any; y: any } } => {
return (
!!loc &&
typeof loc === "object" &&
"coordinates" in loc &&
"x" in loc.coordinates &&
"y" in loc.coordinates
);
};
if (itemLocationCheck(itemLocation)) {
console.log(itemLocation.coordinates.x);
console.log(itemLocation.coordinates.y);
console.log(itemLocation.coordinates.z);
}
The execution of the code above results in a compilation error:
TSError: ⨯ Unable to compile TypeScript:
src/index.ts(20,40): error TS2339: Property 'z' does not exist on type '{ x: any; y: any; }'.
The critical part of the structural check we are doing comes from the usage of the is
keyword in the return type of itemLocationCheck
.
Let's recap what the is
keyword does in TypeScript. We define the following type predicate as the return type of itemLocationCheck
instead of just using a boolean
return type:
loc is { coordinates: { x: any; y: any } }
Using the type predicate has the giant benefit that when itemLocationCheck
is called, if the function returns a truthy value, TypeScript will narrow the type to { coordinates: { x: any; y: any } }
in any block guarded by a call to itemLocationCheck
as explained by StackOverflow contributor Aries Chui.
The type narrowing done by is
creates the error within the if
block because for TypeScript, within that block, itemLocation
has a coordinates
property and that coordinates
property only has the properties x
and y
. It doesn't matter that outside of the scope of that block itemLocation.coordinates.z
exists and is defined.
If we put z
back in the return type predicate while still not performing the "z" in loc.coordinates
check, TypeScript would not throw a compilation error:
let itemLocation: unknown = {
coordinates: { x: 10, y: "cows", z: true }
};
const itemLocationCheck = (
loc: any
): loc is { coordinates: { x: any; y: any; z: any } } => {
return (
!!loc &&
typeof loc === "object" &&
"coordinates" in loc &&
"x" in loc.coordinates &&
"y" in loc.coordinates
);
};
if (itemLocationCheck(itemLocation)) {
console.log(itemLocation.coordinates.x);
console.log(itemLocation.coordinates.y);
console.log(itemLocation.coordinates.z);
}
In essence, what TypeScript wants us to do when using the unkown
type is for us to make it known before we use it. We do not modify the type of the element but we do modify how TypeScript sees it.
Let's explore next how we can do this using type assertion.
Performing a Type Assertion
We can also satisfy TypeScript unknown
check by doing a type assertion. Let's use again our base example but this time let's start itemLocation
with the unknown
type:
let itemLocation: unknown = {
coordinates: { x: 10, y: "cows", z: true }
};
console.log(itemLocation.coordinates.x);
console.log(itemLocation.coordinates.y);
console.log(itemLocation.coordinates.z);
As expected, we get a compilation error. Let's then create a KnownStructure
type and use it to assert the type of itemLocation
:
type KnownStructure = { coordinates: { x: any; y: any; z: any } };
let itemLocation: unknown = {
coordinates: { x: 10, y: "cows", z: true }
};
console.log((itemLocation as KnownStructure).coordinates.x);
console.log((itemLocation as KnownStructure).coordinates.y);
console.log((itemLocation as KnownStructure).coordinates.z);
Instead of an error, this time around the code compiles and we get our desired output:
10
cows
true
Now, we can create a printLocation
function that attempts to print itemLocation
in lowercase. We know that this won't work because itemLocation
is not a string. However, we can make TypeScript believe it is of type string
through type assertion, allowing compilation but then throwing an error:
type KnownStructure = { coordinates: { x: any; y: any; z: any } };
let itemLocation: unknown = {
coordinates: { x: 10, y: "cows", z: true }
};
console.log((itemLocation as KnownStructure).coordinates.x);
console.log((itemLocation as KnownStructure).coordinates.y);
console.log((itemLocation as KnownStructure).coordinates.z);
const printLocation = (loc: string) => {
console.log(loc.toLowerCase());
};
printLocation(itemLocation as string);
console.log(loc.toLowerCase());
^
TypeError: loc.toLowerCase is not a function
Conclusion
I recommend starting to use unknown
instead of any
for handling responses from APIs that may have anything on the response. Having unknown
in place forces us to check for the integrity and structure of the response and promotes defensive coding.
Since #typescript 3.0 is now out here's a tip for using unknown vs any. If you don't care about the type, use any. If you're not 100% sure about the underlying type, such as parsing a JSON response from the server, use unknown.
— Charlie Koster (@ckoster22) July 31, 2018
About Auth0
Auth0 by Okta takes a modern approach to customer identity and enables organizations to provide secure access to any application, for any user. Auth0 is a highly customizable platform that is as simple as development teams want, and as flexible as they need. Safeguarding billions of login transactions each month, Auth0 delivers convenience, privacy, and security so customers can focus on innovation. For more information, visit https://auth0.com.