Fundamental concepts to know as a Typescript beginner - Part 1
This is not a conventional blog on the basics of Typescript but unique things you should know as a beginner.
These are the concepts I learned while doing the Frontend Masters TypeScript Fundamentals, v3 course which helped me to understand Typescript better. I hope it will help you too.
This is not a conventional blog on the basics of Typescript but unique things you should know as a beginner. While writing this blog, I assume that you are already familiar with the basics of Typescript. If not, I recommend you go through the introduction article first.
Let's get started!
1. Type Inference
Let's look at a code example and understand what type inference means:
let firstName = "Prerana"
Here, typescript will infer that the type of firstName
is a string
because we are initializing it with a string value while declaring the variable.
Type inference occurs when we don't explicitly mention the variable type, like in statically typed languages such as C or Java. For example,
String firstName = "Prerana" // In Java
let firstName:string = "Prerana" // In Typescript
A variable in TypeScript is born with its type. Now, if we try to change the type of firstName
to any other kind, the typescript will give us an error.
Type Inference can occur when initializing variables while assigning function parameter values and function return types and even we are initializing objects with multiple varieties.
2. Literal Types
Let's try to declare a constant value in typescript and examine how it infers its type.
const weekDays = 7
What do you guys think would be the type of weekDays
? A number
, maybe? 🤔
No, the type of weekDays
is 7
, which is exactly what's called literal types. In the above example of type inference, firstName
can be any string, but here weekDays
can only represent one possible number which is 7
.
Type 7
is called a literal type.
Let's look at a more helpful example,
const responseCall = (
data: { name: string; age: number },
statusText: "success" | " error"
) => {
console.log(data, status);
};
responseCall({ name: "Prerana", age: 19 }, "success"); // Successful
responseCall({ name: "Prerana", age: 19 }, "err"); // Error
Here's the type of statusText
can only be either a success
or an error
.
If we try to pass another text, it will fail and give us an error.
3. Type Annotations
Suppose we want to declare a variable but not initialize it simultaneously. Is there any way we can safeguard our variable? How will typescript infer it?
Let's see what happens.
let endDate;
Since typescript doesn't have enough information about the variable, it will infer it with one of the most flexible type: any
.
But, if we want to safeguard a variable, we should add a type annotation to it.
The declared data type of a variable cannot be changed by using a different type to change its value. If you try to do so, the typescript compiler will show an error.
Note: It is not mandatory to add type annotations every time since we have type inference in typescript.
4. Type guards
Type guards are used to narrow the variable type and detect runtime failures. They are executed in the runtime and return a boolean value.
In Typescript, a variable can move from a less precise type to a more objective type. This process is called Type Narrowing.
Let's look at an example to understand more:
const multiplication = (a: number, b: number, c?: number): number => {
if (typeof c !== "undefined") {
return a * b * c;
}
return a * b;
};
Here, the if
conditional block is a type guard. Since the c
parameter is optional. We must check whether or not c
is undefined and return the value depending on it. There are multiple ways we can use type guard methods:
Literal ways such as
===
/==
/!==
/!=
instanceof
keywordtypeof
keywordin
keywordEquality narrowing
Custom type guards
5. Excess property checking
A Typescript excess property check ensures an object does not contain any extra properties over those defined in the type annotation.
For example,
Here, we see an error since age is not defined as a parameter which is an excess property. Excess property checking is triggered whenever TypeScript encounters an object literal in an assignment or argument.
However, there are certain limitations to it.
- Using type assertions. Typescript won't trigger excess property checking when we assert a type to an object.
- Using another variable. Typescript won’t check for excess properties as type
Person
is a subset of the type inferred for objectp1
, which is inferred to include all properties in Person plus age. This is known as duck-typing.
6. Index signatures
Objects can be accessed with strings and numbers to contain references to other objects. When only the key and value types are known, typescript uses index signatures to type unknown objects.
Let's see it with an example:
In place of the property name, we place the type of the key inside the square brackets: { [key: keyType]: valueType }
.
Note: The key of the index signature can only be a string, number, or symbol. Other types are not allowed and will throw an error.
7. Discriminated Unions
Discriminated unions are when we have two different types with a similar literal
type which can be used to distinguish between them.
Here's an example:
Please do not mistake this with a string type that has two different values; it is a literal type. The state is a literal type. Since the first case is a success, the typescript automatically suggests only the error value in the else block.
As a result, TypeScript will do a type narrowing considering that the object must be of the type with that specific literal. Discriminated unions are also called Tagged Unions.
8. Open Interfaces
In Typescript, you can have multiple Interfaces declarations in the same scope, which is impossible with type aliases. The Typescript compiler allows reopening interface declarations, i.e., merging several declarations of the same interface into a single declaration. It is useful when adding new fields to an existing interface. This is seen more often when you are using some library types or global window objects.
interface Error {
exampleProperty: string;
}
const getError: Error = {
name: "Type Error",
exampleProperty: "example",
};
name
and message
are the original properties that are non-optional on the global Error
object. The interface Error
includes all the fields originally declared, plus the new field exampleProperty
that we declared individually by us. Both declarations have been merged.
9. Recursive type
Recursive types are auto-referential types with infinite nesting possibilities. The use of infinitely nestable arrays of numbers is one example. In other words, it is circularly referenced.
type NestedNumbers = number | NestedNumbers[];
let nums: NestedNumbers = [3, 4, [5, 6, [7], 59], 221];
It will produce an error if we try to push a string
type.
10. what is .d.ts?
d
stands for Declaration Files that only contain type information. These files don't produce javascript output. They are automatically generated at build time, separating type information and javascript code.
For example,
// serviceCall.ts
export const responseCall = (
data: { name: string; age: number },
status: "success" | " error"
) => {
return 'some code'
};
responseCall({ name: "Prerana", age: 19 }, "success");
// serviceCall.d.ts
export declare const responseCall: (data: {
name: string;
age: number;
}, status: "success" | " error") => string;
It's time to wrap things up for today. Thank you very much for your patience.
There's so much to learn, but it's all achievable if you don't give up.
Let me know one new concept you learned from this blog that you weren't aware of. I would love to know your answer and feedback :)