TypeScript Learning Adventures: A Tale of Love and Hate - Core Types, Tuples, and Enums
In this part, we will focus on TypeScript types.
There are many things to say about TypeScript types but let's start with simple ones.
Core types
Typescript provides many types.
Although JavaScript recognizes some data types, Typescript adds many more, and as you will see later in this article, Typescript also allows you to build your types.
Let's start with some of the core types that JavaScript is already familiar with and that Typescript also supports.
We'll also look at the distinction between JavaScript, which knows the type, and what Typescript by using that type means.
String
We have the string
data type, which is text. A string
can be defined in one of three ways:
- with single quotes,
- with double quotes,
- with backticks.
The final notation with backticks is a specific syntax, available in both current JavaScript and Typescript, that allows us to write template literals.
Template literals are ordinary strings into which you can dynamically insert data:
const name = 'Mensur';
const greeting = `Hello my name is ${name}`;
// Hello my name is Mensur
So, strings are simply text, and JavaScript is aware of string
value types. Typescript performs the same thing.
Here is an example:
function printCake(name: string){
console.log(`Cake name: ${name}`);
}
printCake('Cheesecake');
// Cake name: Cheesecake
Number
The number
type is one of the most important types in JavaScript and Typescript.
Now, in JavaScript, like in Typescript, there is only one number type. Integers and floats have no specific type.
[7, 7.7, -7]
Instead, these values would all be of the number
type:
- 7 is a number without a decimal point
- 7.7 is a decimal number
- -7 is a negative number
Some programming languages, such as Java or C#, include integer
, float
, or double
types.
JavaScript doesn't have that and Typescript doesn't have it either.
So we have the number
type, which we are familiar with from JavaScript, as a type in Typescript.
Here is an example:
function printCake(name: string, price: number){
console.log(`Cake name: ${name}, price: ${price} USD`);
}
printCake('Cheesecake', 7);
// Cake name: Cheesecake, price: 7 USD
One thing to keep in mind is that in TypeScript, you always work with types like string
or number
.
It is string
and number
, and not String
, Number
, and so on.
TypeScript's core primitive types are all lowercase!
Boolean
The boolean
data type is one of the main data types that JavaScript understands and Typescript also supports.
It is always true
or false
, which is critical in programming. Especially when using if
statements.
We have these two values, which is significant because, in JavaScript, you may also be familiar with the concepts of truthy and falsy values. For instance, you may know that the value 0 is a falsy value. Zero is treated as false if you use it in an if condition.
This truthy-falsy idea has nothing to do with data types. That is work done behind the scenes.
Here is an example:
function printCake(name: string, price: number, isGlutenFree: boolean){
const gfMessage = isGlutenFree ? 'yes' : 'no';
console.log(`Cake name: ${name}, price: ${price} USD, gluten free: ${gfMessage}`);
}
printCake('Cheesecake', 7, false);
// Cake name: Cheesecake, price: 7 USD, gluten free: no
Object
In JavaScript, objects look like this:
const cake = {
name: 'Cheesecake',
price: 7,
isGlutenFree: false,
}
There are curly brackets in there, followed by key-value pairs.
With TypeScript, such values are likewise handled as object
types, therefore any JavaScript object
is of type object
.
const cake: object = {
name: 'Cheesecake',
price: 7,
isGlutenFree: false,
}
Nevertheless, there are more specialized versions of objects in TypeScript, so you can state that this is not just any object
, but one that must have these properties or be based on this or that constructor method.
In JavaScript, we can also attempt to access a property that does not exist in the object. Ingredients, for example:
const cake = {
name: 'Cheesecake',
price: 7,
isGlutenFree: false,
}
console.log(cake.ingredients)
TypeScript will not be thrilled, and thus VS Code informs me:
property 'ingredients' does not exist on type '{name: string, price: number, isGlutenFree: boolean}'
So, if we save that and then try to compile it, we get the same issue.
However, VS Code does not assist me in identifying the attributes I may access on this object:
As a result, we should be more explicit by specifying an object type.
We do this by putting a colon after the variable name and then putting curly braces around the object
properties.
const cake: {
name: string,
price: number,
isGlutenFree: boolean
} = {
name: 'Cheesecake',
price: 7,
isGlutenFree: false,
}
As a result, this does not create a new JavaScript object
. This is removed from the compiled JavaScript code. Instead, this is simply TypeScript's notation of a customized object type.
We can add entries here, but not key-value, but key-type.
Therefore, in this case, we may declare that the object
that defines cake
has a name
property, and the value of that name property is of type string, and so on.
With this, we can see that VS Code can now also provide us with a list of the properties that this object has:
Array
In JavaScript, we also have arrays, which are a highly significant data structure.
Arrays are constructed in the following way:
const ingredients = ['eggs', 'flour', 'cream cheese']
Any type of data can be stored there, including number
, string
, boolean
, object
, and other arrays. You can nest arrays and mix data types within arrays, for example, an array with string
and number
mixed.
Arrays are also supported by TypeScript. Every JavaScript array is supported, and the array types can be flexible or strict.
Let's pretend our cake
has an ingredients
property, but we can also create variable or constant ingredients
outside of the object. Using arrays within and outside of objects is the same.
const cake = {
name: "Cheesecake",
price: 7,
isGlutenFree: false,
ingredients: ["eggs", "flour"],
};
const ingredients = ["eggs", "flour"];
When we hover over the ingredients
, TypeScript correctly detects the string array type:
This is a new syntax for you, but it's how TypeScript expresses an array of data.
You have square brackets, and the type of data is stored in front of them.
We can also assign this type to the variable and utilize it in the following ways:
let ingredients: string[];
ingredients = ['eggs', 'flour'];
If we try to do some dirty tricks like this:
let ingredients: string[];
ingredients = ['eggs', 'flour', 7];
it will produce an error:
Type 'number' is not assigneable to type 'string'
So we have a mixed array presently. Because it's an array of strings and numbers, that won't work and isn't supported here.
One way for supporting such a mixed array would be to use any
here.
The any
type is a particular type in TypeScript that we'll look at more closely later. It means "do whatever you want".
Another essential aspect of arrays in TypeScript is support for array data types functions, thus consider the following example:
Therefore ingredient
is correctly identified as a string
because since we are traversing an array of strings, the individual values must also be strings.
As a result, TypeScript provides us with excellent support down there, allowing us to do anything with an ingredient
that can be done with a string
because it knows for certain that ingredient
will be a string due to the types we define there.
That's a fantastic feature that makes creating code much easier, as well as much more flexible and safe.
For example, if it attempted to access the ingredient.toPrecision
, we would receive an error. The toPrecision
method is available on numeric types but not on string types.
Tuple
TypeScript introduces a few new concepts and types not found in vanilla JavaScript, such as the tuple type.
Tuples, which you may be familiar with from other programming languages, do not exist in JavaScript.
For instance, consider the following tuple:
const category = [3, 'dessert'];
You would think, well, this is an array.
It is an array, however, it is a fixed-length array with a fixed type as well as a fixed length.
And where might this come in handy?
Assume we have a property category on our cake
, which is an array with precisely two members:
- first is a number identifier, such as the number 3,
- second is a string identifier, a human-readable description of that identifier, such as "dessert".
As we hover over this category
variable, TypeScript infers an unusual type that we haven't seen before:
(string | number)[]
This implies that TypeScript recognizes that we have an array that may include strings or numbers. This is a union type, which we'll look at later.
The main point is that TypeScript recognizes this as an array of these types of values.
The disadvantage is that we could run the following code:
const cake = {
name: "Cheesecake",
price: 7,
isGlutenFree: false,
ingredients: ["eggs", "flour"],
category: [3, 'dessert']
};
cake.category.push('soup')
cake.category[1] = 2;
Putting the new value into the array is not appropriate for this use case, and it may not make sense because we only require two elements.
Yet, TypeScript is unaware that we only want 2 elements.
We'd also be able to change the category
and the second element (on index 1) will become a number
, and we don't want that in this use case.
This will work since TypeScript just understands that the category
should be of type string
or number
array, and therefore assigning a number
here to the second element and then replacing it with a number
would be permitted because we're simply stating something about the types we may use in there.
So, a tuple would be ideal in this situation:
const cake: {
name: string;
price: number;
isGlutenFree: boolean;
ingredients: string[];
category: [number, string];
} = {
name: "Cheesecake",
price: 7,
isGlutenFree: false,
ingredients: ["eggs", "flour"],
category: [3, "dessert"],
};
We utilize square brackets again, but this time we include a number
, a comma, and then a string
inside the square brackets.
This is now a tuple type.
A tuple is a specific TypeScript construct, and in JavaScript, it will be a standard array, but during TypeScript development, we will encounter code errors as previously mentioned.
What do tuples do now?
Tuple informs TypeScript that we want a specific array with exactly two elements in this situation. I have exactly two kinds in there; the first should be a number
and the second should be a string
.
Now, related to these two lines of code:
cake.category.push('soup')
cake.category[1] = 2;
the second line will result in a TypeScript error:
Type 'number' is not assignable to type 'string'.
However, the first line will not throw an error, which is unusual.
We're arguing that the category
should only have two entries, therefore how come we may add "soup" to the category
array here?
Method .push
is an acceptable exception in tuples.
Unfortunately, TypeScript cannot catch this problem, but it does ensure that we are not providing the incorrect value here, and we also gain some length support outside of push
.
This is something to be aware of, although having TypeScript support is still quite useful.
Hence, if you need exactly X number of values in an array and you know the type of each value in advance, you might want to consider a tuple instead of an array.
This will allow you to incorporate even more strictness into your program, making it clearer about the type of data you're working with and expecting.
Enum
The concept of having a pair of distinct identifiers, global constants you could be working with within your app, that you want to represent as numbers but to which you want to attach a human-readable label is somewhat related to the concept of a Tuple.
You have the enum
type for that.
Again, this exists in certain programming languages, but JavaScript is unaware of it.
Enums look like this:
enum Animal {
CAT,
DOG,
BIRD
};
You use the enum
keyword, which is unique to TypeScript and not JavaScript, followed by curly brackets and then your identifiers.
Therefore, in the end, these identifiers are simply translated to numbers beginning with zero, where you may deal with human-readable labels within your code.
Assume we want to set a price range for our cake:
- the budget-friendly choice should have an ID of 0,
- the mid-range option should have an ID of 1,
- and the luxury option should have an ID of 2.
We could do something like this:
const cake = {
name: "Cheesecake",
price: 7,
isGlutenFree: false,
ingredients: ["eggs", "flour"],
category: [3, "dessert"],
pricing: 0
};
One disadvantage is that we may always add a number
for which we may not have a pricing
category, and attempting to extract the pricing and using the if check later in our code may result in errors.
Also, as developers, we are having difficulty understanding the pricing
category of this cake.
Was it the mid-range? Or was it budget-friendly?
After longer breaks, transferring teams and projects, and so on, you may forget this.
As a developer, you could want human-readable identifiers like "BUDGET_FRIENDLY" or "LUXURY" or something along those lines:
const cake = {
name: "Cheesecake",
price: 7,
isGlutenFree: false,
ingredients: ["eggs", "flour"],
category: [3, "dessert"],
pricing: "BUDGET_FRIENDLY"
};
Now, of course, we could use it, we could operate with such string
values.
The issue is that if we later wanted an if check, we would check if the pricing is equal to, was it "BUDGET-FRIENDLY"?
Was that a single word?
Was this done with underscores?
You see, we have to remember how we wrote these strings and what text is in there. Because the string with the dashes between the words is not the same as the one with underscores.
So string
IDs have drawbacks as well.
In such instances, global constants are commonly defined in JavaScript:
const BUDGET_FRIENDLY = 0;
const cake = {
name: "Cheesecake",
price: 7,
isGlutenFree: false,
ingredients: ["eggs", "flour"],
category: [3, "dessert"],
pricing: BUDGET_FRIENDLY
};
The benefit of this is that we can utilize it in any part of our project.
The disadvantage is that the pricing
is now presumed to be just a number, so we could store any number in there, even one that we don't support. Furthermore, we must define all of these constants and maintain them.
This is acceptable, but an enum
simplifies things a lot:
enum Pricing {
BUDGET_FRIENDLY,
MID_RANGE,
LUXURY
};
Because the enum
is also a custom type, we create it with the enum
keyword and name it Pricing
(the standard is to start with an uppercase character).
Behind the scenes, the BUDGET_FRIENDLY
option is assigned the number 0, MID_RANGE
option 1, and LUXURY
option 2.
Enum values containing all uppercase letters are common. This is not required, you can use whatever value name you choose.
And then, just like on an object, you can use Pricing.BUDGET_FRIENDLY
by entering your identifier:
const cake = {
name: "Cheesecake",
price: 7,
isGlutenFree: false,
ingredients: ["eggs", "flour"],
category: [3, "dessert"],
pricing: Pricing.BUDGET_FRIENDLY
};
You can also utilize specified enums in other places in your code, such as to perform an if
condition:
if(cake.pricing === Pricing.LUXURY){
// do something
}
When we compile the TypeScript code with the defined enum
, we get the following in JavaScript:
var Pricing;
(function (Pricing) {
Pricing[Pricing["BUDGET_FRIENDLY"] = 0] = "BUDGET_FRIENDLY";
Pricing[Pricing["MID_RANGE"] = 1] = "MID_RANGE";
Pricing[Pricing["LUXURY"] = 2] = "LUXURY";
})(Pricing || (Pricing = {}));
So this is an IIFE, a self-executing function.
And, in the end, pricing
is simply controlled as an object with the properties BUDGET_FRIENDLY
, MID_RANGE
, and LUXURY
. Each property then keeps our number values (0, 1, 2) here.
So it's a little more complicated than that, but that's what TypeScript does when it compiles the code to recreate this enum
construct in JavaScript code.
One thing to remember is that you are not limited to the default behavior of enums.
If you don't want to start with zero for some reason, you can put an equal sign here after your identifier and enter any other number:
enum Pricing {
BUDGET_FRIENDLY = 5,
MID_RANGE,
LUXURY
};
BUDGET_FRIENDLY
is now allocated to the number 5, and the identifiers after the identifier where you assign the value pick up on that and simply increment this beginning value.
You are also not limited to numbers, you can also use text or mix the two, though this is not recommended.
That is the strength of the enum
, and it is a great construct if you need human-readable identifiers with some mapping value behind the scenes.
That concludes the essential types. In the following articles, we will learn about more sophisticated and advanced types.
Comments ()