Skip to content

Instantly share code, notes, and snippets.

@ellyxir
Last active February 19, 2025 14:52
Show Gist options
  • Save ellyxir/4cece86ff1a98e9b98315b946f4fa766 to your computer and use it in GitHub Desktop.
Save ellyxir/4cece86ff1a98e9b98315b946f4fa766 to your computer and use it in GitHub Desktop.
My TypeScript Journey

General

  • Javascript needs "return"
  • using an optional property doesn't alway give a warning. using in string interpolation emits no error.
  • arrays ( ["foo", "bar"] ) are objects, so when using them as types, you can't just narrow using typeof, call Array.isArray()
  • Cheatsheets: https://www.typescriptlang.org/cheatsheets/

Javascript

  • var: The var statement declares function-scoped or globally-scoped variables, optionally initializing each to a value.
  • let: lexically scoped
  • const: immutable
  • throw: throw new Error("Not implemented yet!");
  • "" (empty string), null both evaluate to false when used in a truthy expression (if "" ... is false)
  • JavaScript object keys are always coerced to a string, so obj[0] is always the same as obj["0"].
  • if you call a function with more arguments than there are parameters, the extra arguments are simply ignored.
function foo(fn: (num: number, factor: number) => number, num: number): number {
  return fn(num, 10);
}

## Type parameters in Generic constraints
```ts
function get<T, K extends keyof T>(o: T, k: K) {
  return o[k];
}

console.log( get({foo: 3, bar: 4}, "foo") );

// where did factor go???? function same_as_double(n: number) { return n * 2; } console.log( foo(same_as_double, 10) )


## Compiler options
* --noEmitOnError: wont create js file if there is an error
* --target es2015: (aka ES6) newer version that has string interpolation
* --strict: turns on all strict checks (high level flag)
* --noImplicitAny: will not infer type as "any"
* --strictNullChecks: just what it sounds like

## Types
* annoying as hell
  * undefined
  * void
  * null
  * unknown
  * never
* string
* number
* boolean: `true`| `false`
* Array: `string[], Array<string>`
* any
* unknown: like any except you cant actually use it
* functions
  * default values: `function f(x = 10) // ...`
* Object: { x: number; y: number }
  * optional property: use ? after property name `{ x: number; y?: number }`
  * use `foo?` on the variable to not check if its undefined, will return undefined
* Promise: `Promise<string>`
 ```typescript
 async function getFavoriteNumber(): Promise<number> {
  return 26;
}
  • Enum: will do later
  • Symbols: type is "symbol" , creation: Symbol("foo")
    • Making a type from literal symbol
// create a type for a list of literal symbols

const e_run = Symbol("run");
const e_stop = Symbol("stop");
const e_pause = Symbol("pause");
const invalid_symbol = Symbol("invalid_symbol");
type OperationType = typeof e_run | typeof e_stop | typeof e_pause;

function modify_process(pid: number, op: OperationType) {
  console.log(`pid is ${pid}, operation=${String(op)}`)
}

// does not work
modify_process(33, bad_symbol);

// any of the other symbols work
modify_process(33, e_run);
  • BigInt: type is "bigint", creation: BigInt(34030)

Lambda functions

let fancy_names: Array<string> = names.map(function (name) {
  return `${name} the Third`;
});
let uppers = names.map(s => s.toUpperCase());
console.log(uppers);

Union Types

You need to narrow a type when using functions that don't work on all of the types in the union.

function print_id(
  id: 
    | string 
    | number 
    | {id: string}
) {
  switch (typeof id) {
    case "string": 
      console.log("here is your id " + id.toLowerCase());
      break;
    case "number":
      console.log("here is your id " + id.toFixed());
      break;
    default:
      console.log("here is your id " + id);
  }
}

print_id("DFOIJSDOIFJSDFdfsdf");
print_id({id: "foobar"});
print_id(29.348348);
  • useful function: Array.isArray(x)

Type Assertions

const myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
const myCanvas = <HTMLCanvasElement>document.getElementById("main_canvas");

// IF YOU WANT TO JUST YEET
const a = expr as any as T;

literal type inference

TS will infer all literal strings to be strings and not the literal string type.

type BookNames = "war of the worlds" | "hunger games";


function print_book(name: BookNames) {
  return `book=${name}`
}

// BROKEN: infers to "string"
let bookName = "war of the worlds";

// OK: now is the literal string type
let bookName: BookNames = "war of the worlds";

// OK - type assertion
let bookName = "war of the worlds" as "war of the worlds";

// OK - use `const` to cast as literals
let bookName = "war of the worlds" as const;

console.log(print_book(bookName));

sample get type code

const string_t = Symbol("string");
const array_t = Symbol("array");
const object_t = Symbol("object");
const null_t = Symbol("null");
type type_t = typeof array_t | typeof object_t | typeof null_t | typeof string_t;
function get_type(x: object | string | null) : type_t {
  let type_x = typeof x;
  if (type_x === "string")
    return string_t;
  if (Array.isArray(x))
    return array_t;
  if (x === null) 
    return null_t;
  return object_t;
}

type UnitTest = {l: any, r: any};
let tests : Array<UnitTest> = [
  {l: get_type(null), r: null_t}, 
  {l: get_type(""), r: string_t},
  {l: get_type([]), r: array_t},
  {l: get_type({}), r: object_t}
];
console.log(tests.filter(e => e.l !== e.r));

guards

  • in ("swim" in animal)
  • instanceof
  • custom predicate (yes, this is a long example):
type Animal = {
  health: number,
}
type Bear = {
  maul: (animal: Animal) => number,
};
type Fish = {
  bubble_attack: (animal: Animal) => number,
}
let fish: Fish = {
  bubble_attack: animal => animal.health - 1,
}
let bear: Bear = {
  maul: animal => animal.health - 10
};
let rabbit: Animal = {
  health: 15
};
function attack(attacker: Bear | Fish, target: Animal) : number {
  if (isBear(attacker))
    return attacker.maul(target);
  return attacker.bubble_attack(target);
}
function isBear(x: Bear | Fish | Animal): x is Bear {
  return !! (x as Bear).maul
}
console.log(attack(fish, rabbit));
  • discriminated unions: When every type in a union contains a common property with literal types, TypeScript considers that to be a discriminated union, and can narrow out the members of the union.
  • never (used in switch default statements to make sure you were exhaustive)
type Shape = Circle | Square;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

function overloads

function len(s: string): number;
function len(arr: any[]): number;
// below is the implementation signature, it must be compatible with the above "overload signatures"
// note you cannot call the implementation signature directly!
function len<T>(x: T[] | string) {
  return x.length;
}

rest parameters and arguments

// rest parameters 
function multiply(n: number, ...v: Array<number>): number[] {
  return v.map(e => e * n);
}
console.log(multiply(2,1,2,3,4,5))

// example of passing in enumerable
function mySum(...nums: number[]) {
  return nums.reduce((acc: number, cVal:number) => acc + cVal);
}

// call mySum with variable number of "rest" arguments
console.log(mySum(1,2,3));

// call mySum with an enumerable (array) but notice we prefix with `...`
console.log(mySum(...[1,2,3]));

Parameter Destructuring / Pattern Matching

interface User {
  id: string,
  health: number,
}

let frank: User = {
  id: "frank",
  health: 100,
};

function poison({health}: User): number {
  return  health - 5;
}


let new_health = poison(frank);
console.log(new_health);

void as return type for functions

// void return type for functions
type VoidFunc = (x: number) => void;
const double: VoidFunc = (a: number) => { return a * 2 };

// cannot use the return value from a void function even though
// the function is allowed to return a value!
let n = double(20) * 2;
console.log(n);

Objects

  • optional properties, in a type or interface you can make a property as optional with a ?
  • default values function foo({name, dmg=10}) note this uses deconstruction
  • readonly properties: you can mark a property in an interface or type as readonly, TS wont let you update that property. BUT it will let you update subproperties. you need to mark subproperties as readonly also if that is what you want!
  • Object.freeze() will not allow further changes to the object (immutability)
  • Two types are STILL compatible even if one has readonly properties and the other doesn't. This means that the "readonly" check can be bypassed if you pass in a compatible type that isn't readonly. example:
type InventoryID = number;

type Creature = {
  id: String,
  str: number;
  health: number,
  dmg?: number,
  readonly inventory: {
    objs: Array<InventoryID>,
    size: number,
  }
}

type CreatureReadOnly = {
  readonly id: String,
  readonly str: number;
  readonly health: number,
  readonly dmg?: number,
  readonly inventory: {
    objs: Array<InventoryID>,
    size: number,
  }
}

const dummy_target: Creature = {
  id: "Target Dummy",
  inventory: {objs: [1,2,3], size: 10},
  str: 2,
  health: 100,
  dmg: 0,
}

const rabbit: Creature = {
  id: "Cute Rabbit",
  inventory: {objs: [1,2,3], size: 10},
  str: 3,
  health: 20,
  dmg: 1,
}

const readRabbit: CreatureReadOnly = rabbit;
rabbit.id = "new rabbit";
rabbit.inventory.size = 23;
console.log(readRabbit);
  • index signatures: basically maps/dictionaries with all the same return type
// Index Signatures
type Name = string;
interface AgeBook {
  [index: Name]: number;
  age: number
}

function getAge(age_book: AgeBook, name: string): number {
  const age = age_book[name];
  return age === undefined ? 0 : age;
}

const ageBook: AgeBook = {
  age: 25,
  "John": 30,
  "Alice": 25,
  "Bob": 11,
};

let johns_age: number = getAge(ageBook, "Jacob");
console.log(johns_age);
  • excess property checks
interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  return {
    color: config.color || "red",
    area: config.width ? config.width * config.width : 20,
  };
}
// we are passing opacity which triggers an "excess property check", use "as" to bypass strict checking
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
  • using index signatures to overcome excess property checks?
// overcoming excess property checks 
interface SquareConfig {
  color?: string;
  width?: number;
  [index: string]: string | number | undefined;
}
 
function createSquare(config: SquareConfig): { color: string; area: number } {
  const opacity = (typeof config.opacity == "number") ? config.opacity : 0;

  return {
    color: config.color || "blue",
    area: config.width ? 
      config.width * config.width + opacity : 
      20 + opacity
  };
}
 
let mySquare = createSquare({ shininess: undefined, opacity: 3, width: 100 });
console.log(mySquare);
  • const assertion on tuple will make it a readonly tuple let point = [3, 4] as const;

keyof, what the heck is it?

// keyof example
type Person = {
  name: string,
  age: number,
  height: number
}

// keyof just grabs the keys of the type and makes a new type
// that is the union of those strings
// these two following types are equivalent
type PersonKeys = keyof Person;
type CompatiblePersonKeys = "name" | "age" | "height";

function foo(x: CompatiblePersonKeys) {
  console.log(x);
}
const x: PersonKeys = "age";
foo(x);

classes

  • field names that start with # are private
  • creating a class: new ClassName(parameters)
  • TS class: c: { new (): Type }
  • function create<T>(c: { new (): T} ): T { return new c(); }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment