Week 9.2 Introduction to Typescript
In this lecture, Harkirat offers a brief introduction to TypeScript
, covering language classifications, the importance of strong typing, and an overview of TypeScript’s execution. The lecture includes insights into the TypeScript compiler, implementing basic types, and understanding the distinctions between Interfaces
and Types
Types of Languages
1] Loosely Typed Languages
- Runtime Type Association: Data types are associated with values at runtime. Unlike strongly typed languages, type information is not strictly bound during compilation but rather at the time of execution.
- Dynamic Type Changes: Variables can change types during execution, offering more adaptability. This flexibility allows for a dynamic approach to variable assignments and operations.
- Runtime Error Discovery: Type errors may be discovered during runtime, potentially leading to unexpected behaviors. This characteristic provides more freedom but requires careful handling.
- Examples of Loosely Typed Languages: JavaScript, Python, Ruby
C++ Code (Doesn’t Work ❌)
#include <iostream>
int main() { int number = 10; number = "text"; // Error: Cannot assign a string to an integer variable return 0;}
Explanation:
- C++ is a statically-typed language, meaning variable types must be declared and are enforced at compile-time.
- In the given code,
number
is declared as an integer (int
), and attempting to assign a string (“text”) to it results in a compile-time error. - The type mismatch between the declared type and the assigned value leads to a compilation failure.
2] Strongly Typed Languages
- Compile-Time Enforcement: The data type of a variable is strictly enforced during compilation. This means that the compiler checks and ensures that variables are used in a way that is consistent with their types at compile time.
- Type Safety: The compiler or interpreter guarantees that operations are performed only on compatible types. This ensures that type-related errors are caught early in the development process.
- Early Error Detection: Type errors are identified and addressed at compile-time, providing early feedback to developers. This leads to increased reliability and reduces the likelihood of runtime errors.
- Examples of Strongly Typed Languages: Java, C#, TypeScript
JavaScript Code (Does Work ✅)
function main() { let number = 10; number = "text"; // Valid: JavaScript allows dynamic typing return number;}
Explanation:
- JavaScript is a dynamically-typed language, allowing variables to change types during runtime.
- In the provided JavaScript code,
number
is initially assigned the value10
(a number), and later, it is assigned the value"text"
(a string). - JavaScript allows this flexibility, and the code executes without type-related errors.
Considerations:
- Statically-typed languages like C++ provide early error detection during compilation, ensuring type consistency.
- Dynamically-typed languages like JavaScript offer flexibility but may require careful handling to avoid unexpected runtime errors.
The choice between strongly typed and loosely typed languages depends on project requirements, developer preferences, and the balance between early error detection and flexibility during development. Each type has its advantages and considerations, influencing their suitability for specific use cases.
Typescript
Why Typescript
JavaScript is a powerful and widely used programming language, but it has a dynamic typing system, which means variable types are determined at runtime. While dynamic typing provides flexibility, it can lead to runtime errors that are challenging to catch during development.
What Typescript
In response to these challenges, Microsoft introduced TypeScript, a superset of JavaScript that adds static typing to the language. TypeScript is designed to address some of the limitations of JavaScript by providing developers with a more robust type system.
How Typescript
- Static Typing:
- TypeScript introduces static typing, allowing developers to declare the types of variables, parameters, and return values at compile-time.
- Static typing helps catch potential errors during development, offering a level of code safety that may not be achievable in pure JavaScript.
- Compatibility with JavaScript:
- TypeScript is a superset of JavaScript, meaning that any valid JavaScript code is also valid TypeScript code.
- Developers can gradually adopt TypeScript in existing JavaScript projects without the need for a full rewrite.
- Tooling Support:
- TypeScript comes with a rich set of tools and features for development, including code editors (like Visual Studio Code) with built-in TypeScript support.
- The TypeScript compiler (tsc) translates TypeScript code into plain JavaScript, allowing it to run in any JavaScript environment.
- Enhanced IDE Experience:
- IDEs (Integrated Development Environments) that support TypeScript offer improved code navigation, autocompletion, and better refactoring capabilities.
- TypeScript’s type information enhances the overall development experience.
- Interfaces and Type Declarations:
- TypeScript introduces concepts like interfaces and type declarations, enabling developers to define clear contracts for their code.
- Interfaces help document the shape of objects, making it easier to understand and maintain the code.
- Compilation:
- TypeScript code is transpiled to JavaScript during the compilation process, ensuring that the resulting code is compatible with various JavaScript environments and browsers.
Overall, TypeScript provides developers with the benefits of static typing while preserving the flexibility and features of JavaScript. It has gained popularity in large-scale applications and projects where maintaining code quality and catching errors early are crucial.
Execution of TypeScript Code
TypeScript code doesn’t run natively in browsers or JavaScript environments. Instead, it undergoes a compilation process to generate equivalent JavaScript code. Here’s an overview of how TypeScript code is executed:
-
Writing TypeScript Code:
- Developers write TypeScript code using
.ts
or.tsx
files, employing TypeScript’s syntax with features like static typing, interfaces, and type annotations.
- Developers write TypeScript code using
-
TypeScript Compiler (tsc):
- The TypeScript Compiler (
tsc
) is a command-line tool that processes TypeScript code. - Developers run
tsc
to initiate the compilation process.
- The TypeScript Compiler (
-
Compilation Process:
- The TypeScript Compiler parses and analyzes the TypeScript code, checking for syntax errors and type-related issues.
- It generates equivalent JavaScript code, typically in one or more
.js
or.jsx
files.
-
Generated JavaScript Code:
- The output JavaScript code closely resembles the original TypeScript code but lacks TypeScript-specific constructs like type annotations.
- TypeScript features that aren’t present in JavaScript (e.g., interfaces) are often transpiled or emitted in a way that doesn’t affect runtime behavior.
-
JavaScript Execution:
- The generated JavaScript code can now be executed by any JavaScript runtime or browser.
- Developers can include the resulting JavaScript files in HTML documents or use them in Node.js environments.
-
Runtime Environment:
- In the chosen runtime environment, the JavaScript code is interpreted or compiled by the JavaScript engine (e.g., V8 in Chrome, SpiderMonkey in Firefox).
- Just-in-time (JIT) compilation or interpretation occurs to convert the code into machine code that the computer’s processor can execute.
-
Interacting with the DOM (Browser Environments):
- In browser environments, the JavaScript code, generated from TypeScript, may interact with the Document Object Model (DOM) to manipulate web page structure and behavior.
TypeScript Compiler (tsc
)
- The TypeScript Compiler (
tsc
) is responsible for transpiling TypeScript code into JavaScript. - It is a part of the official TypeScript distribution and can be installed using tools like npm.
- Developers run
tsc
from the command line, specifying the TypeScript file(s) they want to compile. - Configuration for the compilation process can be provided via a
tsconfig.json
file. - The compiler performs type checking, emits JavaScript files, and allows customization of compilation options.
In summary, TypeScript code is transformed into JavaScript through the TypeScript Compiler (
tsc
). This compilation process ensures that TypeScript’s features are compatible with existing JavaScript environments, enabling developers to benefit from static typing during development while still producing standard JavaScript for execution.
In addition to the TypeScript Compiler (tsc
), several alternative tools have gained popularity for their efficiency, speed, and additional features when transpiling TypeScript to JavaScript. Here are a couple of noteworthy ones:
- esbuild: a highly performant JavaScript bundler and minifier, but it also supports TypeScript.
- swc (Speedy Web Compiler): a fast and low-level JavaScript/TypeScript compiler.
Setting up a Typescript Nodejs Application
Let’s walk through the process of setting up a simple TypeScript Node.js application locally on your machine.
Step 1 - Install TypeScript Globally:
npm install -g typescript
This command installs TypeScript globally on your machine, allowing you to use the tsc
command anywhere.
Step 2 - Initialize a Node.js Project with TypeScript:
mkdir node-appcd node-appnpm init -ynpx tsc --init
These commands create a new directory (node-app
), initialize a Node.js project with default settings (npm init -y
), and then generate a tsconfig.json
file using npx tsc --init
.
Step 3 - Create a TypeScript File (a.ts):
const x: number = 1;console.log(x);
Step 4 - Compile the TypeScript File to JavaScript:
tsc -b
The -b
flag tells TypeScript to build the project based on the configuration in tsconfig.json
. This generates a JavaScript file (index.js
) from the TypeScript source (a.ts
).
Step 5 - Explore the Generated JavaScript File (index.js
):
const x = 1;console.log(x);
Note that the generated JavaScript file doesn’t include TypeScript-specific code. It’s standard JavaScript without types.
Step 6 - Attempt to Assign a String to a Number:
let x: number = 1;x = "harkirat";console.log(x);
Step 7 - Try Compiling the Code Again:
tsc -b
Upon compiling, TypeScript detects the type error (x
being assigned a string) and reports it in the console. Additionally, no index.js
file is generated due to the type error.
This example illustrates one of TypeScript’s key benefits: catching type errors at compile time. By providing static typing, TypeScript enhances code reliability and helps identify potential issues before runtime. This is particularly valuable in large codebases where early error detection can save time and prevent bugs.
Basic Types in Typescript
In TypeScript, basic types serve as the building blocks for defining the data types of variables. Here’s an overview of some fundamental types provided by TypeScript:
- Number:
-
Represents numeric values.
-
Example:
let age: number = 25;
-
- String:
-
Represents textual data (sequences of characters).
-
Example:
let name: string = "John";
-
- Boolean:
-
Represents true or false values.
-
Example:
let isStudent: boolean = true;
-
- Null:
-
Represents the absence of a value.
-
Example:
let myVar: null = null;
-
- Undefined:
-
Represents a variable that has been declared but not assigned a value.
-
Example:
let myVar: undefined = undefined;
-
Problems and Code Implementation
1] Hello World Greeting
Objective:
Learn how to give types to function arguments in TypeScript.
Task:
Write a TypeScript function named greet
that takes a user’s first name as an argument and logs a greeting message to the console.
Function Signature:
function greet(firstName: string): void { // Implementation goes here}
Solution:
function greet(firstName: string): void { console.log("Hello " + firstName);}
// Example Usagegreet("harkirat");
Explanation:
- Function Definition (
function greet(firstName: string): void
):- The
greet
function is declared with a parameter namedfirstName
. : string
indicates that thefirstName
parameter must be of type string.: void
specifies that the function does not return any value.
- The
- Function Body (
console.log("Hello " + firstName);
):- Inside the function body, a
console.log
statement prints a greeting message to the console. - The message includes the provided
firstName
parameter.
- Inside the function body, a
- Function Invocation (
greet("harkirat");
):- The function is called with the argument
"harkirat"
. - The provided argument must be a string, aligning with the specified type in the function definition.
- The function is called with the argument
This example demonstrates the basic usage of TypeScript types in function parameters, ensuring that the expected data type is enforced and catching errors related to type mismatches during development.
2] Sum Function
Objective:
Learn how to assign a return type to a function in TypeScript.
Task:
Write a TypeScript function named sum
that takes two numbers as arguments and returns their sum. Additionally, invoke the function with an example.
Function Signature:
function sum(a: number, b: number): number { // Implementation goes here}
Solution:
function sum(a: number, b: number): number { return a + b;}
// Example Usageconsole.log(sum(2, 3));
Explanation:
- Function Definition (
function sum(a: number, b: number): number
):- The
sum
function is declared with two parameters,a
andb
, both of type number. : number
indicates that the function returns a value of type number.
- The
- Function Body (
return a + b;
):- Inside the function body, the sum of
a
andb
is calculated using the+
operator. - The result is then returned.
- Inside the function body, the sum of
- Function Invocation (
console.log(sum(2, 3));
):- The function is called with the arguments
2
and3
. - The result is logged to the console using
console.log
.
- The function is called with the arguments
This example showcases how to specify the return type of a function in TypeScript, ensuring that the function returns the expected data type. In this case, the
sum
function returns a number.
3] Age Verification Function
Objective:
Understand Type Inference in TypeScript.
Task:
Write a TypeScript function named isLegal
that takes an age
as a parameter and returns true
if the user is 18 or older, and false
otherwise. Also, invoke the function with an example.
Function Signature:
function isLegal(age: number): boolean { // Implementation goes here}
Solution:
function isLegal(age: number): boolean { if (age > 18) { return true; } else { return false; }}
// Example Usageconsole.log(isLegal(22)); // Output: true
Explanation:
- Function Definition (
function isLegal(age: number): boolean
):- The
isLegal
function is declared with a parameterage
of type number. : boolean
indicates that the function returns a boolean value.
- The
- Function Body (
if (age > 18) {...}
):- Inside the function body, an
if
statement checks if the providedage
is greater than 18. - If true, the function returns
true
; otherwise, it returnsfalse
.
- Inside the function body, an
- Function Invocation (
console.log(isLegal(22));
):- The function is called with the argument
22
. - The result (
true
) is logged to the console usingconsole.log
.
- The function is called with the argument
This example demonstrates how TypeScript’s type inference can be leveraged. The return type (
boolean
) is implicitly inferred based on the conditions within the function. TheisLegal
function is designed to return a boolean value indicating whether the provided age is 18 or older.
4] Delayed Function Execution
Objective:
Learn to work with functions as parameters in TypeScript.
Task:
Write a TypeScript function named delayedCall
that takes another function (fn
) as input and executes it after a delay of 1 second. Also, invoke the delayedCall
function with an example.
Function Signature:
function delayedCall(fn: () => void): void { // Implementation goes here}
Solution:
function delayedCall(fn: () => void): void { setTimeout(fn, 1000);}
// Example UsagedelayedCall(function() { console.log("hi there");});
Explanation:
- Function Definition (
function delayedCall(fn: () => void): void
):- The
delayedCall
function is declared with a parameterfn
of type function that takes no arguments and returnsvoid
. : void
indicates that the function doesn’t return any value.
- The
- Function Body (
setTimeout(fn, 1000);
):- Inside the function body,
setTimeout
is used to delay the execution of the provided function (fn
) by 1000 milliseconds (1 second).
- Inside the function body,
- Function Invocation (
delayedCall(function() {...});
):- The
delayedCall
function is invoked with an anonymous function as an argument. - The provided function logs “hi there” to the console after a 1-second delay.
- The
This example illustrates how TypeScript handles functions as first-class citizens, allowing them to be passed as arguments to other functions. The
delayedCall
function provides a way to execute a given function after a specified delay.
The tsconfig.json
File in TypeScript
The tsconfig.json
file in TypeScript is a configuration file that provides settings for the TypeScript compiler (tsc
). It allows you to customize various aspects of the compilation process and define how your TypeScript code should be transpiled into JavaScript.
Below are a bunch of options that you can change to change the compilation process in the tsconfig.json
file:
1] Target Option in tsconfig.json
:
The target
option in a tsconfig.json
file specifies the ECMAScript target version to which the TypeScript compiler (tsc
) will compile the TypeScript code. It allows you to define the lowest version of ECMAScript that your code should be compatible with. Here’s an explanation and example usage:
-
ES5 (ECMAScript 5):
-
When
target
is set to"es5"
, the TypeScript compiler generates code compatible with ECMAScript 5, which is widely supported across browsers. -
Example:
{"compilerOptions": {"target": "es5",// Other options...}} -
TypeScript Code:
const greet = (name: string) => `Hello, ${name}!`; -
Output:
"use strict";var greet = function (name) { return "Hello, ".concat(name, "!"); };
-
-
ES2020 (ECMAScript 2020):
-
When
target
is set to"es2020"
, the TypeScript compiler generates code compatible with ECMAScript 2020, incorporating the latest features. -
Example:
{"compilerOptions": {"target": "es2020",// Other options...}} -
TypeScript Code:
const greet = (name: string) => `Hello, ${name}!`; -
Output:
"use strict";const greet = (name) => `Hello, ${name}!`;
-
By setting the
target
option, you ensure that the generated JavaScript code adheres to the specified ECMAScript version, allowing you to control the level of compatibility and take advantage of the features available in newer ECMAScript versions.
2] rootDir:
-
The
rootDir
option in atsconfig.json
file specifies the root directory where the TypeScript compiler (tsc
) should look for.ts
files. -
It is considered a good practice to set
rootDir
to the source folder (src
), indicating the starting point for TypeScript file discovery. -
Example:
{"compilerOptions": {"rootDir": "src",// Other options...}}
3**] outDir
**
-
The
outDir
option defines the output directory where the TypeScript compiler will place the generated.js
files. -
It determines the structure of the output directory relative to the
rootDir
. -
Example:
{"compilerOptions": {"outDir": "dist",// Other options...}}
If
rootDir
is set to"src"
andoutDir
is set to"dist"
, the compiled files will be placed in thedist
folder, mirroring the structure of thesrc
folder.
4] noImplicitAny
-
The
noImplicitAny
option in atsconfig.json
file determines whether TypeScript should issue an error when it encounters a variable with an implicitany
type. -
Enabled (
"noImplicitAny": true
):{"compilerOptions": {"noImplicitAny": true,// Other options...}}Example:
// Compilation Error: Implicit any typeconst greet = (name) => `Hello, ${name}!`; -
Disabled (
"noImplicitAny": false
):{"compilerOptions": {"noImplicitAny": false,// Other options...}}No error will be issued for implicit
any
types.
5] removeComments
-
The
removeComments
option in atsconfig.json
file determines whether comments should be included in the final JavaScript output. -
Enabled (
"removeComments": true
):{"compilerOptions": {"removeComments": true,// Other options...}}Comments will be stripped from the generated JavaScript files.
-
Disabled (
"removeComments": false
):{"compilerOptions": {"removeComments": false,// Other options...}}Comments will be retained in the generated JavaScript files.
These options provide flexibility and control over the compilation process, allowing you to structure your project and handle type-related scenarios according to your preferences.
Interfaces
In TypeScript, an interface is a way to define a contract for the shape of an object. It allows you to specify the expected properties, their types, and whether they are optional or required. Interfaces are powerful tools for enforcing a specific structure in your code.
Understanding Interfaces
Suppose you have an object representing a user:
const user = { firstName: "harkirat", lastName: "singh", email: "email@gmail.com", age: 21,};
To assign a type to the user
object using an interface, you can create an interface named User
:
interface User { firstName: string; lastName: string; email: string; age: number;}
Now, you can explicitly specify that the user
object adheres to the User
interface:
const user: User = { firstName: "harkirat", lastName: "singh", email: "email@gmail.com", age: 21,};
Explanation:
- Interface Declaration (
interface User
):- The
interface
keyword is used to declare an interface namedUser
. - Inside the interface, you define the expected properties (
firstName
,lastName
,email
,age
) along with their types.
- The
- Assigning Type to Object (
const user: User = { /* ... */ };
):- By stating
const user: User
, you are explicitly indicating that theuser
object must adhere to the structure defined by theUser
interface. - If the
user
object deviates from the defined structure or misses any required property, TypeScript will raise a compilation error.
- By stating
Assignment 1
Problem: Create a function isLegal
that returns true or false if a user is above 18. It takes a user as an input.
Solution:
// Define an interface to specify the structure of a user objectinterface User { firstName: string; lastName: string; email: string; age: number;}
// Create a function 'isLegal' that checks if a user is above 18function isLegal(user: User): boolean { // Check if the user's age is greater than 18 if (user.age > 18) { return true; // Return true if the user is legal } else { return false; // Return false if the user is not legal }}
Code Explanation:
- An interface “User” is defined to enforce the structure of a user object with properties: firstName, lastName, email, and age.
- The function “isLegal” takes a user object as input and checks if the user’s age is greater than 18.
- It returns true if the user is legal (age > 18) and false otherwise.
Assignment 2
Problem: Create a React component that takes todos as an input and renders them.
Solution:
// Define an interface to specify the structure of a todo objectinterface TodoType { title: string; description: string; done: boolean;}
// Define the input prop for the Todo componentinterface TodoInput { todo: TodoType;}
// Create a React component 'Todo' that takes a 'todo' prop and renders itfunction Todo({ todo }: TodoInput): JSX.Element { return ( <div> <h1>{todo.title}</h1> <h2>{todo.description}</h2> {/* Additional rendering logic can be added for other properties */} </div> );}
Code Explanation:
- An interface “TodoType” is defined to specify the structure of a todo with properties: title, description, and done.
- An interface “TodoInput” is defined to specify the input prop for the Todo component.
- The React component “Todo” takes a prop “todo” of type “TodoType” and renders its properties (title and description).
Implementing Interfaces
In TypeScript, you can implement interfaces using classes. This provides a way to define a blueprint for the structure and behavior of a class. Let’s take an example:
Assume you have a Person
interface:
interface Person { name: string; age: number; greet(phrase: string): void;}
Now, you can create a class that adheres to this interface:
class Employee implements Person { name: string; age: number;
constructor(n: string, a: number) { this.name = n; this.age = a; }
greet(phrase: string) { console.log(`${phrase} ${this.name}`); }}
Here’s what’s happening:
- The
Employee
class implements thePerson
interface. - It has properties (
name
andage
) matching the structure defined in the interface. - The
greet
method is implemented as required by the interface.
This approach is handy when creating various types of persons (like Manager, CEO), ensuring they all adhere to the same interface contract. It maintains consistency in the structure and behavior across different classes.
Types
In TypeScript, types allow you to aggregate data together in a manner very similar to interfaces. They provide a way to define the structure of an object, similar to how interfaces do. Here’s an example:
type User = { firstName: string; lastName: string; age: number;};
Features
-
Unions: Unions allow you to define a type that can be one of several types. This is useful when dealing with values that could have different types. For instance, imagine you want to print the ID of a user, which can be either a number or a string:
type StringOrNumber = string | number;function printId(id: StringOrNumber) {console.log(`ID: ${id}`);}printId(101); // ID: 101printId("202"); // ID: 202Unions provide flexibility in handling different types within a single type definition.
-
Intersection: Intersections allow you to create a type that has every property of multiple types or interfaces. If you have types like
Employee
andManager
, and you want to create aTeamLead
type that combines properties of both:type Employee = {name: string;startDate: Date;};type Manager = {name: string;department: string;};type TeamLead = Employee & Manager;const teamLead: TeamLead = {name: "harkirat",startDate: new Date(),department: "Software Developer"};Intersections provide a way to create a new type that inherits properties from multiple existing types.
In summary, while types and interfaces are similar in defining object structures, types in TypeScript offer additional features like unions and intersections, making them more versatile in certain scenarios.
Interfaces vs Types
Major Differences
1. Declaration Syntax:
- Type:
- Uses the
type
keyword. - More flexible syntax, can represent primitive types, unions, intersections, and more.
- Uses the
- Interface:
- Uses the
interface
keyword. - Typically used for defining the structure of objects.
- Uses the
2. Extension and Merging:
- Type:
- Supports extending types.
- Can’t be merged; if you define another type with the same name, it will override the previous one.
- Interface:
- Supports extending interfaces using the
extends
keyword. - Automatically merges with the same-name interfaces, combining their declarations.
- Supports extending interfaces using the
3. Declaration vs. Implementation:
- Type:
- Can represent any type, including primitives, unions, intersections, etc.
- Suitable for describing the shape of data.
- Interface:
- Mainly used for describing the shape of objects.
- Can also be used to define contracts for classes.
Other Differences
- Type Overriding:
- Types cannot be overridden or merged. Redefining a type with the same name replaces the previous one.
- Interfaces automatically merge if declared with the same name.
- Object Literal Strictness:
- Types are more lenient when dealing with object literal assignments.
- Interfaces enforce strict object literal shapes.
- Implementation for Classes:
- Interfaces can be used to define contracts for class implementations.
- Types are more versatile for creating complex types and reusable utility types.
When to Use Which
- Use Types:
- For advanced scenarios requiring union types, intersections, or mapped types.
- When dealing with primitive types, tuples, or non-object-related types.
- Creating utility types using advanced features like conditional types.
- Use Interfaces:
- When defining the structure of objects or contracts for class implementations.
- Extending or implementing other interfaces.
- When consistency in object shape is a priority.
Examples
Type Example:
type StringOrNumber = string | number;
function printId(id: StringOrNumber) { console.log(`ID: ${id}`);}
printId(101); // ID: 101printId("202"); // ID: 202
Interface Example:
interface Employee { name: string; startDate: Date;}
interface Manager { name: string; department: string;}
type TeamLead = Employee & Manager;
const teamLead: TeamLead = { name: "Harkirat", startDate: new Date(), department: "Software Developer",};
In summary, choose types for flexibility and advanced type features, and use interfaces for object shapes, contracts, and class implementations, ensuring a consistent and readable codebase.