Typescript
TypeScript is a statically typed superset of JavaScript developed by Microsoft. It adds optional static types, interfaces, and type inference to JavaScript, enabling better tooling, such as code completion and refactoring, and reducing runtime errors. TypeScript code compiles to plain JavaScript, making it compatible with any environment that runs JavaScript.
Getting Started
Bun and Deno are Javascript runtimes that provide a simple way to run a typescript file.
Using Bun:
bun run index.ts
Using Deno:
deno run index.ts
Topics
- Compile time vs run time checking
- Typescript types
- Type assertions
- instanceof and typeof
- Union Types
- Intersection Types
- Type Aliases
- Interfaces
Compile time vs run time checking
Compile time type checking is performed by the TypeScript compiler, which checks the types of variables and expressions at compile time. This helps catch errors early and provides better tooling support.
function add(a: number, b: number): number {
return a + b;
}
add(1, 2); // Valid
add(1, '2');
// Compile-time error: Argument of type 'string'
// is not assignable to parameter of type 'number'.
Run time type checking is performed by the JavaScript runtime, which checks the types of variables and expressions at runtime. This allows for more flexibility and dynamic behavior, but may result in runtime errors if the types are not correct.
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Both arguments must be numbers');
}
return a + b;
}
add(1, 2); // Valid
add(1, '2'); // Runtime error: Both arguments must be numbers
Typescript types
Primitive types
let number: number = 1;
let string: string = 'Hello, world!';
let boolean: boolean = true;
let nullValue: null = null;
let undefinedValue: undefined = undefined;
let symbol: symbol = Symbol('mySymbol'); // unique and immutable
let bigInt: bigint = BigInt(10); // BigInt is a new type in ES2020
Object types
//interface
interface User {
name: string;
age: number;
}
//class
class User {
name: string;
age: number;
}
//enum
enum Color { Red, Green, Blue }
//array types
let arrayOfNumbers: number[] = [1, 2, 3];
//tuple types
let tuple: [number, string] = [1, 'Hello'];
Other types
//any
let any: any = 1;
//object: the parameter's type is an object type
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
//unknown
function f2(a: unknown) {
// Error: Property 'b' does not exist on type 'unknown'.
a.b();
}
//never
function error(message: string): never {
throw new Error(message);
}
Type assertions
let a: number = 1;
a = '1'; // error
let b: number = 1;
b = '1' as number; // error
let c: number = 1;
c = '1' as const; // error
Non null assertion operator (!
)
let name: string | null = null;
// we use the non-null assertion operator
// to tell the compiler that name will never be null
let nameLength = name!.length;
Type assertion and union types.
type Person = {
name: string
age: number
}
type Employee = {
companyId: number
role: string
}
var worker: Employee | Person = {
companyId: 1,
role: 'worker',
name: 'John Doe',
age: 30,
}
console.log((worker as Person).name)
console.log((worker as Employee).companyId)
instanceof and typeof
The typeof
operator is used to determine the type of a variable. It returns a string indicating the type of the operand.
let x = 42;
console.log(typeof x); // "number"
let y = 'Hello, World!';
console.log(typeof y); // "string"
let z;
console.log(typeof z); // "undefined"
The instanceof
operator is used to check if an object is an instance of a specific class or constructor function.
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
let person = new Person('John', 30)
console.log(person instanceof Person) // true
Literal Types
literal types allow you to specify exact values a variable can hold. They are a subset of more general types (like string
, number
, or boolean
).
//string literal type
let direction: "up" | "down" | "left" | "right";
direction = "up";
direction = "down";
direction = "left";
direction = "right";
//number literal types
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceRoll;
roll = 1;
roll = 2;
roll = 7; //type '7' is not assignable to type 'DiceRoll
//boolean literals types
type YesNo = true | false;
let answer: YesNo;
answer = true; // valid
answer = false; // valid
answer = "yes"; // error: Type '"yes"' is not assignable to type 'YesNo'
Union Types
Union types are a way to represent a value that can be one of several possible types. They are denoted by using the |
symbol.
//union type
let a: number | string = 'Hello'
//union type as parameter
function fn(a: number | string) {
console.log(a)
}
//union type as return type
function fn(): number | string {
return 'Hello'
}
//union type as property
class Person {
name: number | string
age: number | string
constructor(name: number | string, age: number | string) {
this.name = name
this.age = age
}
}
Intersection Types
Intersection types are used to combine multiple types into a single type. They allow you to define a type that has all the properties of multiple types.
interface Person {
name: string
age: number
}
interface Employee {
companyId: number
role: string
}
type EmployeeOrPerson = Employee & Person
const worker: EmployeeOrPerson = {
companyId: 1,
role: 'worker',
name: 'John Doe',
age: 30,
}
Type Aliases
Type aliases are a way to create a new name for an existing type. They are useful when you want to make your code more readable or when you want to create a new type that is similar to an existing one.
type Name = string;
type Age = number;
type User = { name: Name; age: Age };
type fn = (name: Name, age: Age) => void;
type calc = (a: number, b: number, action: (result: number) => void) => void;
Interfaces
Interfaces are a way to define a contract for an object. They specify the properties and methods that an object must have.
//basic interface declaration
interface Person {
name: string;
age: number;
}
//optional properties
interface Person {
name: string;
age?: number;
}
//readonly properties
interface Person {
readonly name: string;
age: number;
}
//method signature
interface Person {
sayHello(): void;
}
//function types
interface GreetFunction {
(name: string): string;
}
//indexable types
interface StringArray {
[index: number]: string;
}
//extending interfaces
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
employeeId: number;
}
//hybrid types
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
let counter: Counter = (function (start: number) {
// function implementation
}) as Counter;
counter.interval = 123;
counter.reset = function () {
// reset implementation
};
Class implementation
Classes can implement interfaces to enforce a particular structure.
interface Person {
name: string;
age: number;
greet(): string;
}
class Employee implements Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name}.`;
}
}
Extending Multiple Interfaces
interface Person {
name: string;
}
interface Contact {
email: string;
}
interface Employee extends Person, Contact {
employeeId: number;
}
Interface vs type alias
Feature | Interface π€ | Type Alias π·οΈ |
---|---|---|
Syntax | interface Person { name: string; } | type Person = { name: string; }; |
Usage | Objects, methods | Objects, unions, tuples |
Extending | extends keyword | Intersection (& ) |
Classes | Implementable in classes | Canβt implement directly |
Merging | β | β |
Unions & Intersections | β | β |
Index Signatures | β | β |
Function Types | β | β |
Tuples | β | β |
Computed Properties | β | β |
Constraints | extends for constraints | Generics & intersections |
Use Cases | Best for object structures | Versatile, complex types |
Keyof Operator
The keyof
operator is used to get the keys of an object type.
interface User {
name: string
age: number
location: string
}
type UserKeys = keyof User
const nameKey: UserKeys = 'name'
const ageKey: UserKeys = 'age'
const locationKey: UserKeys = 'location'
console.log(nameKey)
console.log(ageKey)
console.log(locationKey)
This is useful for creating function that can only accept valid property names
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user: User = {
name: 'John',
age: 30,
location: 'New York',
}
const userName = getProperty(user, 'name')
const userAge = getProperty(user, 'age')
const userLocation = getProperty(user, 'location')