Initializing a TypeScript Project
## Install TypeScript globally
npm install -g typescript
## Compile a TypeScript file
tsc file.ts
## Specify the output file
tsc file.ts -out file.js
## Compile automatically when a file changes:
tsc file.ts -w
## Set up a `tsconfig.json` file:
tsc --init
## Compile all TypeScript files and watch for changes:
tsc -w
## Compile all TypeScript files in the `src` directory and output to the `dist` directory:
tsc -p src -outDir dist -wtsconfig.json
{
"compilerOptions": {
...
/* Modules */
"target": "es2016", // Change to "ES2015" to compile to ES6
"rootDir": "./src", // Where to compile from
"outDir": "./public", // Where to compile to (usually the folder to be deployed to the web server)
/* JavaScript Support */
"allowJs": true, // Allow JavaScript files to be compiled
"checkJs": true, // Type check JavaScript files and report errors
/* Emit */
"sourceMap": true, // Create source map files for emitted JavaScript files (good for debugging)
"removeComments": true, // Don't emit comments
},
"include": ["src"] // Ensure only files in src are compiled
}Reference Values
In JavaScript, a primitive value is a data type that is not an object and has no methods. Primitives are immutable and cannot be changed. Each primitive value is stored in a unique memory location.
StringNumberBooleannullundefinedSymbolBigInt
Set the type of a variable using a colon : followed by the type.
let name: string = "John";
let age: number = 30;
let isStudent: boolean = false;
let x: null = null;
let y: undefined = undefined;
let sym: symbol = Symbol("key");
let big: bigint = 100n;
// Declare a variable without assigning a value
let z: string;On the other hand, a reference value is an object that is stored in memory and can be accessed by reference. Reference values can be changed.
let person: {
name: string;
age: number;
isStudent: boolean;
} = {
name: "John",
age: 30,
isStudent: false,
};Arrays
let numbers: number[] = [1, 2, 3, 4, 5];
// Use union types to define an array that can contain different types
let values: (string | number)[] = ["John", 30, "Doe", 40];If you initialise an array with values, it’s not necessary to explicitly state the type, as TypeScript will infer it:
let numbers = [1, 2, 3, 4, 5]; // numbers: number[]A tuple is an array with a fixed number of elements whose types are known.
let person: [string, number, boolean] = ["John", 30, false];Functions
// Define a function called circle that takes a diam variable of type number, and returns a string
function circle(diam: number): string {
return "The circumference is " + Math.PI * diam;
}
console.log(circle(10)); // The circumference is 31.41592653589793Same function with ES6 arrow syntax:
const circle = (diam: number): string => {
return "The circumference is " + Math.PI * diam;
};
console.log(circle(10));Add a question mark ? to make a parameter optional:
function greet(name: string, greeting?: string): string {
return greeting ? `${greeting}, ${name}!` : `Hello, ${name}!`;
}A function that returns void
function log(message: string): void {
console.log(message);
}Dynamic Types
let value: any = 5;
value = "John";
value = true;Type Aliases
type StringOrNumber = string | number;
type PersonObject = {
name: string;
id: StringOrNumber;
};
const person1: PersonObject = {
name: "John",
id: 1,
};
const person2: PersonObject = {
name: "Delia",
id: 2,
};
const sayHello = (person: PersonObject) => {
return "Hi " + person.name;
};
const sayGoodbye = (person: PersonObject) => {
return "Seeya " + person.name;
};Union Types
let id: string | number;
id = "123";
id = 123;Extending a Type
type Animal = {
name: string;
};
type Bear = Animal & {
honey: boolean;
};
const bear: Bear = {
name: "Winnie",
honey: true,
};The DOM and Type Casting
With the non-null assertion operator (!) we can tell the compiler explicitly
that an expression has value other than null or undefined. This is can be
useful when the compiler cannot infer the type with certainty, but we have more
information than the compiler.
// Here we are telling TypeScript that we are certain that this anchor tag exists
const link = document.querySelector("a")!;
console.log(link.href); // www.freeCodeCamp.orgIf we needed to select a DOM element by its class or id, we can use type casting to specify the type of the element.
const form = document.querySelector(".new-item-form") as HTMLFormElement;
const type = document.querySelector("#type") as HTMLSelectElement;
const tofrom = document.querySelector("#tofrom") as HTMLInputElement;TypeScript also has an event object that can be used to access the properties of an event. We can use type casting to specify the type of the event object.
const form = document.getElementById("signup-form") as HTMLFormElement;
form.addEventListener("submit", (e: Event) => {
e.preventDefault();
console.log(e.target);
});Classes
class Person {
name: string;
isCool: boolean;
pets: number;
constructor(n: string, c: boolean, p: number) {
this.name = n;
this.isCool = c;
this.pets = p;
}
sayHello() {
return `Hi, my name is ${this.name} and I have ${this.pets} pets`;
}
}Access Modifiers
We can also use access modifiers to restrict access to class properties and methods.
class Person {
readonly name: string; // This property is immutable - it can only be read
private isCool: boolean; // Can only access or modify from methods within this class
protected email: string; // Can access or modify from this class and subclasses
public pets: number; // Can access or modify from anywhere - including outside the class
constructor(n: string, c: boolean, e: string, p: number) {
this.name = n;
this.isCool = c;
this.email = e;
this.pets = p;
}
sayMyName() {
console.log(`Your not Heisenberg, you're ${this.name}`);
}
}This can be made more concise by using access modifiers in the constructor:
class Person {
constructor(
readonly name: string,
private isCool: boolean,
protected email: string,
public pets: number
) {}
sayMyName() {
console.log(`Your not Heisenberg, you're ${this.name}`);
}
}Classes can also be extended:
class Programmer extends Person {
programmingLanguages: string[];
constructor(
name: string,
isCool: boolean,
email: string,
pets: number,
pL: string[]
) {
super(name, isCool, email, pets);
this.programmingLanguages = pL;
}
}Modules
In the tsconfig.json file, change the following options to support modern importing and exporting:
"target": "es2016",
"module": "es2015"(Although, for Node projects you very likely want "module": "CommonJS" – Node
doesn’t yet support modern importing/exporting.)
In the HTML file, add the type="module" attribute to the script tag:
<script src="app.js" type="module"></script>Use import and export to import and export modules:
// src/hello.ts
export function sayHi() {
console.log("Hello there!");
}
// src/script.ts
import { sayHi } from "./hello.js";
sayHi(); // Hello there!Note: akways use the .js extension when importing modules in the browser.
Interfaces
Interfaces define how an object should look. Interfaces won’t get compiled to JavaScript and add bloat to JavaScript, they are only used by the TypeScript compiler to check types.
interface Person {
name: string;
age: number;
}Using the object:
function sayHi(person: Person) {
console.log(`Hi ${person.name}`);
}
sayHi({
name: "John",
age: 48,
}); // Hi JohnObject types can also be defined anonymously:
function sayHi(person: { name: string; age: number }) {
console.log(`Hi ${person.name}`);
}Interfaces with Functions
Interfaces can also define functions:
interface Greet {
(name: string): string;
}
const greet: Greet = (name: string) => `Hello ${name}`;Interfaces with Classes
interface HasFormatter {
format(): string;
}
class Person implements HasFormatter {
constructor(public username: string, protected password: string) {}
format() {
return this.username.toLocaleLowerCase();
}
}
// Must be objects that implement the HasFormatter interface
let person1: HasFormatter;
let person2: HasFormatter;
person1 = new Person("Danny", "password123");
person2 = new Person("Jane", "TypeScripter1990");
console.log(person1.format()); // dannyInterfaces vs Type Aliases
The key distinction is that type aliases cannot be reopened to add new properties, vs an interface which is always extendable.
interface Animal {
name: string;
}
interface Bear extends Animal {
honey: boolean;
}
const bear: Bear = {
name: "Winnie",
honey: true,
};Adding new fields to an existing interface:
interface Animal {
name: string;
}
// Re-opening the Animal interface to add a new field
interface Animal {
tail: boolean;
}
const dog: Animal = {
name: "Bruce",
tail: true,
};Literal Types
Literal types allow you to specify the exact value a variable can have.
// Union type with a literal type in each position
let favouriteColor: "red" | "blue" | "green" | "yellow";
favouriteColor = "blue";
favouriteColor = "crimson"; // ERROR: Type '"crimson"' is not assignable to type '"red" | "blue" | "green" | "yellow"'.Generics
Generics allow you to have type-safety in components where the arguments and return types are unknown ahead of time.
In TypeScript, generics are used when we want to describe a correspondence between two values.
// <T> is just the convention - e.g. we could use <X> or <A>
function identity<T>(arg: T): T {
return arg;
}
const addID = <T>(obj: T) => {
let id = Math.floor(Math.random() * 1000);
return { ...obj, id };
};We can also specify the type of the generic:
const addID = <T extends { name: string }>(obj: T) => {
let id = Math.floor(Math.random() * 1000);
return { ...obj, id };
};
// ERROR: argument should have a name property with string value
let person2 = addID(["Sally", 26]);
// Explicitly state what type the argument should be between the angle brackets.
let person1 = addID<{ name: string; age: number }>({ name: "John", age: 40 });Using Generics Over any
Problem with using any:
function logLength(a: any) {
console.log(a.length); // No error
return a;
}
let hello = "Hello world";
logLength(hello); // 11
let howMany = 8;
logLength(howMany); // undefined (but no TypeScript error)Using generics:
function logLength<T>(a: T) {
console.log(a.length); // ERROR: TypeScript isn't certain that `a` is a value with a length property
return a;
}Extending Generics
Use generics that extend an interface to ensure that the argument has a length property:
interface hasLength {
length: number;
}
function logLength<T extends hasLength>(a: T) {
console.log(a.length);
return a;
}
let hello = "Hello world";
logLength(hello); // 11
let howMany = 8;
logLength(howMany); // Error: numbers don't have length propertiesWrite a function where the argument is an array of elements that all have a length property:
interface hasLength {
length: number;
}
function logLengths<T extends hasLength>(a: T[]) {
a.forEach((element) => {
console.log(element.length);
});
}
let arr = [
"This string has a length prop",
["This", "arr", "has", "length"],
{ material: "plastic", length: 30 },
];
logLengths(arr);
// 29
// 4
// 30Generics with Interfaces
// The type, T, will be passed in
interface Person<T> {
name: string;
age: number;
documents: T;
}
// We have to pass in the type of `documents` - an array of strings in this case
const person1: Person<string[]> = {
name: "John",
age: 48,
documents: ["passport", "bank statement", "visa"],
};
// Again, we implement the `Person` interface,
// and pass in the type for documents - in this case a string
const person2: Person<string> = {
name: "Delia",
age: 46,
documents: "passport, P45",
};Enums
Enums allow us to define or declare a collection of related values, that can be numbers or strings, as a set of named constants.
Enums are useful when we have a set of related constants. For example, instead of using non-descriptive numbers throughout your code, enums make code more readable with descriptive constants.
enum ResourceType {
BOOK,
AUTHOR,
FILM,
DIRECTOR,
PERSON,
}
console.log(ResourceType.BOOK); // 0
console.log(ResourceType.AUTHOR); // 1
// To start from 1
enum ResourceType {
BOOK = 1,
AUTHOR,
FILM,
DIRECTOR,
PERSON,
}
console.log(ResourceType.BOOK); // 1
console.log(ResourceType.AUTHOR); // 2Enums with Strings
enum ResourceType {
BOOK = "BOOK",
AUTHOR = "AUTHOR",
FILM = "FILM",
DIRECTOR = "DIRECTOR",
PERSON = "PERSON",
}
console.log(ResourceType.BOOK); // BOOK
console.log(ResourceType.AUTHOR); // AUTHORTypeScript Strict Mode
// tsconfig.json
"strict": truenoImplicitAny
With the noImplicitAny option set to true, TypeScript will raise an error when it cannot infer the type of a variable.
// ERROR: Parameter 'a' implicitly has an 'any' type.
function logName(a) {
console.log(a.name);
}strictNullChecks
When the strictNullChecks option is false, TypeScript effectively ignores
null and undefined. This can lead to unexpected errors at runtime.
With strictNullChecks set to true, null and undefined have their own
types, and you’ll get a type error if you assign them to a variable that expects
a concrete value (for example, string).
const getSong = () => {
return "song";
};
let whoSangThis: string = getSong();
const singles = [
{ song: "touch of grey", artist: "grateful dead" },
{ song: "paint it black", artist: "rolling stones" },
];
const single = singles.find((s) => s.song === whoSangThis);
console.log(single.artist); // ERROR: Object is possibly 'undefined'.This forces you to handle the possibility of null or undefined:
if (single) {
console.log(single.artist);
}Narrowing in TypeScript
Using the typeof Operator
function addAnother(val: string | number) {
if (typeof val === "string") {
// TypeScript treats `val` as a string in this block,
// so we can use string methods on `val`
// and TypeScript will not raise an error
return val.concat(" " + val);
}
// TypeScript knows `val` is a number here
return val + val;
}
console.log(addAnother("Woooo")); // Woooo Woooo
console.log(addAnother(20)); // 40We can also give both types a common distinguishing property, with a literal string value:
// All trains must now have a type property equal to 'Train'
interface Train extends Vehicle {
type: "Train";
carriages: number;
}
// All trains must now have a type property equal to 'Plane'
interface Plane extends Vehicle {
type: "Plane";
wingSpan: number;
}
type PlaneOrTrain = Plane | Train;Now TypeScript can narrow down the type based on the type property:
function getVehicleInfo(vehicle: PlaneOrTrain) {
if (vehicle.type === "Train") {
console.log(vehicle.carriages);
} else {
console.log(vehicle.wingSpan);
}
}TypeScript with React
React Props
// src/components/Person.tsx
import React from "react";
const Person: React.FC<{
name: string;
age: number;
}> = ({ name, age }) => {
return (
<div>
<div>{name}</div>
<div>{age}</div>
</div>
);
};
export default Person;Interfaces can also be used to define the props:
interface Props {
name: string;
age: number;
}
const Person: React.FC<Props> = ({ name, age }) => {
return (
<div>
<div>{name}</div>
<div>{age}</div>
</div>
);
};We can then import this component into App.tsx. If we don’t provide the
necessary props, TypeScript will give an error.
import React from "react";
import Person from "./components/Person";
const App: React.FC = () => {
return (
<div>
<Person name="John" age={48} />
</div>
);
};
export default App;React Hooks with TypeScript
useState
We can declare what types a state variable should be by using angle brackets.
Below, if we omitted the angle brackets, TypeScript would infer that cash is a
number. So, if want to enable it to also be null, we have to specify:
const Person: React.FC<Props> = ({ name, age }) => {
const [cash, setCash] = useState<number | null>(1);
setCash(null);
return (
<div>
<div>{name}</div>
<div>{age}</div>
</div>
);
};useRef
useRef returns a mutable object that persists for the lifetime of the
component. We can tell TypeScript what the ref object should refer to – below we
say the prop should be a HTMLInputElement:
const Person: React.FC = () => {
// Initialise .current property to null
const inputRef = useRef<HTMLInputElement>(null);
return (
<div>
<input type="text" ref={inputRef} />
</div>
);
};