TypeScript - herougo/SoftwareEngineerKnowledgeRepository GitHub Wiki

Source:

npm install typescript --save-dev
tsc sandbox.ts  # compile
tsc sandbox.ts -w  # watch the file and compile when saved

tsconfig.json
{
  "include": ["src"],
  "compilerOptions": {
    "outDir": "./build"
  }
}
// Special types
let v: any = true;
v = "string"; // no error as it can be "any" type
Math.round(v); // no error as it can be "any" type
// `unknown` type is similar, but more complicated
// `null` and `undefined` are also types

let firstName: string = "Dylan";

(diameter: number) => {
  return diameter * Math.PI;
}

const names: string[] = [];

// The readonly keyword can prevent arrays from being changed.
const names: readonly string[] = ["Dylan"];
names.push("Jack"); // Error: Property 'push' does not exist on type 'readonly string[]'.

const ourReadonlyTuple: readonly [number, boolean, string] = [5, true, 'The Real Coding God'];

const car: { type: string, model: string, year: number } = {
  type: "Toyota",
  model: "Corolla",
  year: 2009
};

// Example with an optional property
const car: { type: string, mileage?: number } = { // no error
  type: "Toyota"
};
car.mileage = 2000;

// Index signatures can be used for objects without a defined list of properties.
const nameAgeMap: { [index: string]: number } = {};
nameAgeMap.Jack = 25; // no error
nameAgeMap.Mark = "Fifty"; // Error: Type 'string' is not assignable to type 'number'.

// Enums can be strings or numbers
enum CardinalDirections {
  North,  // first element init to 0 and auto-incremented after that
  East,
  South,
  West
}
enum CardinalDirections {
  North = 1,  // set initial value to 1 instead and auto-increment from that
  East,
  South,
  West
}
enum StatusCodes {
  NotFound = 404,
  Success = 200,
  Accepted = 202,
  BadRequest = 400
}

// Aliases: allow defining types with a custom name
type CarYear = number
type CarType = string
type CarModel = string
type Car = {
  year: CarYear,
  type: CarType,
  model: CarModel
}

// Interfaces are similar to type aliases, except they only apply to object types.
interface Rectangle {
  height: number,
  width: number
}

interface ColoredRectangle extends Rectangle {
  color: string
}

const coloredRectangle: ColoredRectangle = {
  height: 20,
  width: 10,
  color: "red"
};

// Union | (OR)
function printStatusCode(code: string | number) {
  console.log(`My status code is ${code}.`)
}


// Functions
function getTime(arg: boolean): number {
  return new Date().getTime();
}

function printHello(): void {
  console.log('Hello!');
}
function pow(value: number, exponent: number = 10) {
  return value ** exponent;
}
function add(a: number, b: number, ...rest: number[]) {
  return a + b + rest.reduce((p, c) => p + c, 0);
}
// Function alias
type Negate = (value: number) => number;

// as keyword: will directly change the typescript type of the given
//             variable (not the actual type though).
let x: unknown = 'hello';
console.log((x as string).length)
// equivalent to
console.log((<string>x).length);

// Classes
// has public, private, and protected modifiers
//  has readonly keyword
class Person {
  private readonly name: string;

  public constructor(name: string) {
    this.name = name;
  }

  public getName(): string {
    return this.name;
  }
}

interface Shape {
  getArea: () => number;
}

class Rectangle implements Shape {
  public constructor(
    protected readonly width: number, // defines and sets property width
    protected readonly height: number) {}

  public getArea(): number {
    return this.width * this.height;
  }
}
// override
class Square extends Rectangle {
  public constructor(width: number) {
    super(width, width);
  }

  // this toString replaces the toString from Rectangle
  public override toString(): string {
    return `Square[width=${this.width}]`;
  }
}

// abstract class
abstract class Polygon {
  public abstract getArea(): number;

  public toString(): string {
    return `Polygon[area=${this.getArea()}]`;
  }
}

class Rectangle extends Polygon {
  ...
}

GENERICS

// with functions
function createPair<S, T>(v1: S, v2: T): [S, T] {
  return [v1, v2];
}
console.log(createPair<string, number>('hello', 42)); // ['hello', 42]

// with classes
class NamedValue<T> {
  private _value: T | undefined;

  ...
}
let value = new NamedValue<number>('myNumber');
value.setValue(10);

// with type aliases
type Wrapped<T> = { value: T };
const wrappedValue: Wrapped<number> = { value: 10 };

// default type values AND extends
class NamedValue<S = string, T extends string> {
  ...
}

OTHER

// Partial changes all the properties in an object to be optional.
let pointPart: Partial<Point> = {};

// Required changes all the properties in an object to be required.
let pointPart: Required<NamedPoint> = {x: 1, y: 2, name: "my_point"};

// Record<string, number> is equivalent to { [key: string]: number }

// Omit removes keys from an object type.
let pointPart: Omit<NamedPoint, 'x' | 'y'> = {name: 'hello'};

// Pick removes all but the specified keys from an object type.
let pointPart: Pick<NamedPoint, 'x' | 'y'> = {x: 1, y: 2};

// Exclude removes types from a union.
type Primitive = string | number | boolean
const value: Exclude<Primitive, string> = true;

// ReturnType extracts the return type of a function type.
// Parameters extracts the parameter types of a function type as an array.
type PointGenerator = (param: number) => { x: number; y: number; };
const point: ReturnType<PointGenerator> = {
  x: 10,
  y: 20
};
const param: Parameters<PointGenerator>[0] = 0;

// Readonly is used to create a new type where all properties are readonly, meaning they cannot be modified once assigned a value.
const person: Readonly<Person> = {
  name: "Dylan",
  age: 35,
};
person.name = 'Israel';

// keyof is a keyword in TypeScript which is used to extract the key type from an object type.
// keyof with explicit keys: When used on an object type with explicit keys,
// keyof creates a union type with those keys.
function printPersonProperty(person: Person, property: keyof Person) {
  ...
}

// keyof can also be used with index signatures to extract the index type.
type StringMap = { [key: string]: unknown };
// `keyof StringMap` resolves to `string` here
function createStringPair(property: keyof StringMap, value: string): StringMap {
  ...
}

NULL

  • By default null and undefined handling is disabled, and can be enabled by setting strictNullChecks to true.
  • When strictNullChecks is enabled, TypeScript requires values to be set unless undefined is explicitly added to the type.

TypeScript Exercises

Source: http://typescript-exercises.github.io/

Question 4 - Type Predicates

function isAdmin(person: Person): person is Admin {
    return person.type === 'admin';
}

https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

Question 6 - Function Overloads

https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads

Remember the last signature is the implementation and must match the previous signatures (e.g. use ?).

function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date

Combine Keys of N Types

type ColorfulPowerfulCircle = Colorful & Circle & {type: 'powerful'};

Arrow Function Typing

const fn = (x: number): string => { return "hi" };

11 - Modules

Learn modules???

declare module 'str-utils' {
    type StrToStr = (input: string) => string;
    export const strReverse: StrToStr;
    export function strConcat(v1: string, v2: string): string;
}

12 - Type Alias vs Generic

???

export type IndexFn = <T>(input: T[], comparator: Comparator<T>) => number;

13 - Namespace

Namespace???

14 - Challenging!

interface MapperFunc<I, O> {
    (): MapperFunc<I, O>;
    (input: I[]): O[];
}

This is an interface for a function that can be called in two different ways.

When to use export in module and when to not???

In TypeScript, the protected keyword in constructor parameters serves two purposes simultaneously: it declares a class property and defines the constructor parameter.

class Hello {
    constructor(protected hi) { }
}

15 - Challenging!

T[K] is a type???

K extends keyof T???

export type ObjectWithNewProp<K extends string, T, V> = T & {[NK in K]: V};???

type ObjectWithNewProp<T, K extends string, V> = T & {[NK in K]: V};

export class ObjectManipulator<T> {
    constructor(protected obj: T) {}

    public set<K extends string, V>(key: K, value: V): ObjectManipulator<ObjectWithNewProp<T, K, V>> {
        return new ObjectManipulator({...this.obj, [key]: value} as ObjectWithNewProp<T, K, V>);
    }

    public get<K extends keyof T>(key: K): T[K] {
        return this.obj[key];
    }

    public delete<K extends keyof T>(key: K): ObjectManipulator<Omit<T, K>> {
        const newObj = {...this.obj};
        delete newObj[key];
        return new ObjectManipulator(newObj);
    }

    public getObject(): T {
        return this.obj;
    }
}

.ts files vs .tsx files

  • Use .ts for pure TypeScript files.
  • Use .tsx for files which contain JSX.

Practical TypeScript

1: TypeScript Compiler is Smart Sometimes

TypeScript compiler is smart enough to recognize the types based on the type key.

export type User = {
    type: "user"
};
export type Admin = {
    type: "admin"
};
export type Person = User | Admin;

function hi(person: Person): string {
    switch(person.type) {
        case 'admin':
            return person.role;
        case 'user':
            return person.occupation;
    }
}

2: Be Careful When Dealing With the Union Type

Be careful when dealing with a union type (e.g. A|B) as an input (and the output is dependent on the type of the input).

Wrong

export type Person = User | Admin;
export type Criteria = Partial<Omit<User, 'type'>> | Partial<Omit<Admin, 'type'>>;

export function filterPersons(persons: Person[], personType: string, criteria: Criteria): Person[] {
    ...
}

Right

export type Person = User | Admin;
export type Criteria = Partial<Omit<User, 'type'>> | Partial<Omit<Admin, 'type'>>;

export function filterPersons(persons: Person[], personType: User['type'], criteria: Partial<Omit<User, 'type'>>): User[];
export function filterPersons(persons: Person[], personType: Admin['type'], criteria: Partial<Omit<Admin, 'type'>>): Admin[];
export function filterPersons(persons: Person[], personType: string, criteria: Criteria): Person[] {
    ...
}

Tips from a Youtube Video (Josh Tried Coding)

source: https://www.youtube.com/watch?v=q5DFpyIN5Xs

Using typeof

Useful for having the IDE look up types for you, or if you simply want to be lazy.

const person = {
	age: 29,
	name: "Joe"
};

type Person = typeof person;
type Person = keyof typeof person; // "age" | "name"

Using Return Type (With and Without Aync)

const func = () => { return 'val'; }
const asyncFunc = async () => { return 'val'; }

type FuncReturn = ReturnType<typeof func> // string
type AsyncFuncReturn = Awaited<ReturnType<typeof asyncFunc>> // string

Prettify Object Types

Problem: See below. NestedType, when hovered over, simply says it is "MainType & {isDeveloper: boolean}", but you want to see ALL of the fields at once.

  • Solution: Use Prettify!
interface MainType = {
	name: string,
	age: number
};

type NestedType = MainType & {
	isDeveloper: boolean	
};

type Prettify<T> = {
	[K in keyof T]: T[K]
} & {}

type PrettifiedNestedType = Prettify<NestedType>
// { name: string, age: number, isDeveloper: boolean }
⚠️ **GitHub.com Fallback** ⚠️