TypeScript
TS is a syntactic super-set of JS which adds static typing
TLDR: JS has a lot of BS, TS adds type safety.
TypeScript uses compile time type checking. Which means it checks if the specified types match before running the code, not while running the code.
A common way to use TS is to use the official TS Compiler, which transpiled TS --> JS.
npm i typescript --save-dev
You can run the compiler with:
npx tsc
You can configure the TS compiler using a tsconfig.json file. It can be generated with:
npx tsc --init
The above is just one option to get started with TS; you can also use create-react-app then check off TS, etc...
npx create-react-app <app_name> --template typescript
npx create vite@latest <app_name> --template react-ts
npm create-next-app@latest --ts
Simple (Primitives) Types
There are three main primitives in JavaScript and TypeScript.
boolean
- true or false valuesnumber
- whole numbers and floating point valuesstring
- text values
Type Assignment
When creating a variable, there are 2 main ways to assign a type:
Explicit
- writing out the type:let firstname: string = "Dev";
easier to read and more intentional
Implicit
- TS will infer the type based on the assigned value:let firstname = "Dylan"
shorter, faster, and more often used when developing and testing
Type Assignment Error
TS will throw an error if data types don't match
// EXPLICIT let firstName: string = "Dev"; // string firstName = 19; // throws error: value is number // IMPLICIT let firstname = "Dev"; firstname = 19 // still throws error
Unable to Infer
TS may not always properly infer the variable type.
In such cases, it will set the type to
any
, which disables type checking
Special Types
TS has special types that may not refer to any specific type of data.
> Type: any
type that disables type checking and allows all types to be used.
let x: any = true; x = "string" // no error as it can be 'any' type Math.round(x) // same as above
> Type: unknown
similar, but safer alternative to any.
TS will prevent unknown types from being used
let w: unknown = 1; w = "string"; // no error w = { runANonExistentMethod: () => { console.log("I think therefore I am"); } } as { runANonExistentMethod: () => void} // How can we avoid the error for the code below when we don't know the type? w.runANonExistentMethod(); // Error: Object is of type 'unknown'.
best used when you don't know the type of data being typed.
To add a type later, you'll need to cast it.
Casting is when we use the "as" keyword to say property or variable is of the casted type
> Type: never
never effectively throws an error whenever it is defined.
// Error: Type 'boolean' is not assignable to type 'never'. let x: never = true;
> Type: undefined & null
undefined
andnull
are types that refer to the JS primitivesundefined
andnull
, respectively.let y: undefined = undefined; let z: null = null;
These types don't have much use unless strictNullChecks
is enabled in the tsconfig.json
file.
Arrays
For Explicit assignment, add a [ ] after the value type (number[ ], ...)
const names: string[] = [];
names.push("Dev") // works just like JS array
// TS will throw type error for incompatable values
names.push(3) // can't push number to string[]
Readonly
The readonly
keyword can prevent arrays from being changed.
const names: readonly string[] = ["Dev", "Nelly"]
names.push("Akon") // error
Type Inference
TS can infer the array type based on its values
const numbers = [1, 2, 3] // inferred to type: number[]
numbers.push("4") // error
// when reading values:
let firstNum: number = numbers[0];
Tuples
array w/ a pre-defined length and types for each index and allow for arrays with different types, all of which are known.
Defining Tuple
To define a tuple, specify the type of each element in the array:
let myTup: [number, boolean, string]; // define types
myTup = [99, true, "HOV"] // initialize must correspond
myTup = [true, "HOV", 5] // ERROR; ORDER MATTERS
Readonly Tuples
It is suggested to make tuples readonly since there's no type safety in the tuple outside the first n indexes with strongly defined types:
// We have no type safety in our tuple for indexes 3+ let ourTuple: [number, boolean, string]; ourTuple.push('4th index is not type safe'); // Instead, use readonly let ourTuple: readonly [number, boolean, string] = [99, true, 'LP'] ourTuple.push('4th index is not type safe'); // ERROR
Named Tuples
Named tuples allow us to provide context for values at each index:
const graph: [x: number, y: number] = [55.2, 41.3];
Destructuring Tuples
Since tuples = arrays, we can also destructor them:
const graph: [number, number] = [1, 2]; // new tuple const [x, y] = graph; // x=1, y=2
Object Types
TS has specific syntax for Objects (python dictionaries). It basically adds a schema for each Object, allowing for inferences and type safety.
const car: {
// SCHEMA
type: string,
model: string,
year: number
} = {
// VALUES
type: "Acura",
model: "NSX",
year: 2017
};
// TS infers types of properties, and will throw errors:
car.year = 2020 // no err
car.year = "2020" // err (year: number)
Optional Properties
properties followed by a ? don't have to be defined during Object definition.
// NO ERROR; 'mileage' is optional const car: { type: string, mileage?: number } = { type: "Toyota", };
Index Signatures
When you don’t know all the names of a type’s properties ahead of time, but you do know the shape of the values, you can use an index signature to describe the types of possible values:
/* StringArray interface has the index signature, which states that when a StringArray is indexed with a number, it will return a string. An index signature property type must be string/number. */ interface StringArray { [index: number]: string; } const myArray: StringArray = getStringArray(); const secondItem = myArray[1];
Enums
enum is a special class that represents a group of 'const' variables. They come in 2 flavours: numeric and string.
1. Numeric Enums
Default Numeric Enums: where enums will initialize the first value to 0 and add 1 to each additional value:
enum myValues { A, // default 0 B, // 1 C, // 2 D // 3 } console.log(myValues.D); // 3
Initialized Numeric Enums: where you set the value of the first numeric enum, and have it auto increment after that:
enum myValues { A = 99, // set 99 B, // 100 C, // 101 D // 102 }
Fully Initialized Numeric Enums: where you set unique numeric values for each enum value (no auto increment)
enum myValues { A = 99, B = 89, C = 79, D = 69 }
2. String Enums
enum Passwords {
Gmail = "abc123",
Github = "abc789"
};
console.log(Passwords.Gmail); // "abc123"
TS allows types to be defined separately from the variables that use them. You can use Aliases & Interfaces to be easily shared b/n different variables/objects.
Type Aliases
doesn’t actually create a new type - it creates a new name to refer to that type.
// ex. Aliasing on Primitives (not really useful) type Second = number; let time: Second = 10; // same as::: let time: number = 10 // ex. Aliasing on Complex types like Objects (helpful) type CarYear = number; type CarType = string; type isCarInsured = boolean; type Car = { year: CarYear; // number type: CarType; // string isInsured: isCarInsured; // boolean } // now you can use this 'Car' type for different Car objects const myCarYear: CarYear = 2001; const myCarType: CarType = "Sportscar"; const isMyCarInsured: isCarInsured = true; const myCar: Car = { year: myCarYear, type: myCarType, isInsured: isMyCarInsured } console.log(myCar.year); // 2001 console.log(myCar.type); // Sportscar console.log(myCar.isInsured); // true
Interfaces
similar to type aliases, except they only apply to OBJECTS
interface Car { year: number, type: string, isInsured: boolean, isTinted?: boolean } let myCar: Car = { year: 2003, type: "Coupe", isInsured: true, } console.log(myCar.type); // Coupe
Interfaces can EXTEND other interfaces (union) using the extends keyword:
nterface Rectangle { height: number, width: number } interface ColoredRectangle extends Rectangle { color: string } const coloredRectangle: ColoredRectangle = { height: 20, width: 10, color:
Union Types
used when a value can be > 1 type
using the | like: | , we are saying that _ is type1 or type2
// @param 'code' can be either string OR number (either works) function printStatusCode(code: string | number) { console.log(`My status code is ${code}.`) } printStatusCode(404); // WORKS printStatusCode('404'); // WORKS // make sure that the code works fine for both type1 and type2; ie. don't call string functions on _ if _ may also be a number.
Functions
TS has specific syntax for adding types to fx parameters & return values:
// Note: TS will infer types if they're not explicit
// syntax
function <myFunc>(<param>: <type>): <returnType> {
return ...
}
{/* Void Return Type*/}
const sayHi = (name: string): void => {
console.log(name)
}
{/*Optional? + Default= Parameters*/}
function addNums(a: number, b?: number = 1): number {
return a + b;
}
{/*Rest Parameters (type must be array)*/}
function add(a: number, b:number, ...rest: number[]) {
return a + b + rest.reduce((p, c) => p + c, 0);
}
Export & Import Functions
Use named exports (unlimited exports in a file):
export function sayHi(): void { ... } // to import: import {sayHi} from './path2file'
Use default exports (1 per file):
export default function sayHi(): void { ... } // to import: import sayHi from './path2abovefile'
If you have a file that does 1 default export & many named exports:
// 👇️ default export export default function sum(a: number, b: number): number { return a + b; }; // 👇️ named export export const example = 'hello world'; // in another file, to import: import sum, { example } from './path2abovefile'
Function Overloading
Overloading - same name, different parameters and return types
Method 1:
function greet(person: string | string[]): string | string[] { if (typeof person === 'string') { return `Hello, ${person}!`; } else if (Array.isArray(person)) { return person.map(name => `Hello, ${name}!`); } } greet('World'); // 'Hello, World!' greet(['Dev', 'Bruce']); // ['Hello, Dev!', 'Hello, Bruce!']
Method 2: Define so-called *overload signatures* & an *implementation signature*
overload signatures don't have a body; they only define params & their types
the implementation signature has a body and defines params & their types
// Overload signatures (unlimited) function greet(person: string): string; function greet(persons: string[]): string[]; // Implementation signature (only 1) function greet(person: unknown): unknown { if (typeof person === 'string') { return `Hello, ${person}!`; } else if (Array.isArray(person)) { return person.map(name => `Hello, ${name}!`); } } greet('World'); // 'Hello, World!' greet(['Dev', 'Bruce']); // ['Hello, Dev!', 'Hello, Bruce!']
Casting
allows you to convert a variable from 1 type to another.
Casting w/ as
let x: unknown = 'hello';
console.log((x as string).length);
// CASTING DOESN'T CHANGE THE TYPE OF THE DATA IN THE VARIABLE
let x: unknown = 4;
console.log((x as string).length); // err (x still holds number)
// TO FORCE CAST, CAST TO 'UNKNOWN'
let x = 'hello';
console.log(((x as unknown) as string).length); // x is not actually a number so this will return undefined
Casting w/ *<>*
// WONT WORK IN .TSX (REACT) !!!
let x: unknown = 'hello';
console.log( (<string>x).length );
Basic Generics
Suppose you have an array; 'A' list of items. You don't know what the items are nor do you care. This means that you have an array of generic A elements. What A is is IRRELEVANT.
Many DS&A do not depend on the actual type of the object. However, you still want to enforce a constraint between various variables. Ie. a list reversal algorithm:
/*
Here you are basically saying that reverse() takes an array (items: T[]) of some type T (notice the type parameter in reverse<T>) and returns an array of type T (notice : T[]).
*/
function reverse<T>(items: T[]): T[] {
let toreturn = [];
for (let i = items.length - 1; i >= 0; i--) {
toreturn.push(items[i]);
}
return toreturn;
}
// ie. passing number[] to reverse() will result in return type number[]
var sample = [1, 2, 3];
var reversed = reverse(sample);
console.log(reversed); // [ 3, 2, 1 ]
// ie. passing string[] to reverse() will result in return type string[]
var strArr = ['1', '2', '3'];
var reversedStrs = reverse(strArr);
console.log(reversedStrs); // [ '3', '2', '1' ]
Type Aliases (and also for Interface)
Generics in type aliases allow creating types that are more reusable.
type Wrapped<T> = { value: T };
const wrappeddNum: Wrapped<number> = { value: 10 };
const wrappeddStr: Wrapped<string> = { value: 'Ten' };
Default Value
Generics can be assigned default values which apply if no other value is specified or inferred.
// default value for Generic = string (type for _value variable which is set by the user)
class NamedValue<T = string> {
private _value: T | undefined;
constructor(private name: string) {}
public setValue(value: T) {
this._value = value;
}
public getValue(): T | undefined {
return this._value;
}
public toString(): string {
return `${this.name}: ${this._value}`;
}
}
let value = new NamedValue('myNumber');
value.setValue('myValue');
console.log(value.toString()); // myNumber: myValue
Extends
Constraints can be added to generics to limit what's allowed
// v1 must be type S=String/Number
// v2 must be type T=String/Number
function createPair<S extends string | number, T extends string | number>(v1: S, v2: T): [S, T] {
console.log(`creating pair: v1='${v1}', v2='${v2}'`);
return [v1, v2];
}
Utility Types
TS comes with a large number of types that can help with some common type manipulation, usually referred to as utility types.
Partial
all the properties in an object are now optional.
interface Point { x: number; y: number; } let pointPart: Partial<Point> = {}; // x and y are optional pointPart.x = 10; console.log(pointPart); // { x: 10 }
Required
all the properties in an object are now required.
interface Car { make: string; model: string; mileage?: number; // mileage is optional here, but Required<Car> makes it required } let myCar: Required<Car> = { make: 'Ford', model: 'Focus', mileage: 12000 // `Required` forces mileage to be defined };
Record
shortcut to defining an object type with a specific key type and value type.
// Record<string, number> ======= { [key: string]: number } const nameAgeMap: Record<string, number> = { 'Alice': 21, 'Bob': 25 };
Omit
removes keys from an object
interface Person { name: string; age: number; location?: string; } const bob: Omit<Person, 'age' | 'location'> = { name: 'Bob' // Omit<Person> has removed age & location from Person type thus they can't be defined here };
Pick
removes all but the specified keys from an object
interface Person { name: string; age: number; location?: string; } const bob: Pick<Person, 'name'> = { name: 'Bob' // Pick<Person, 'name'> has only kept name, so age & location were removed from Person type thus they can't be defined here };
Last updated