TypeScript Reference

Kip Landergren

(Updated: )

My cheat sheet for TypeScript covering resources, FAQs, and code reference.

Contents

Installation

$ npm install --save-dev typescript

Usage

Generate a tsconfig.json file:

$ ./node_modules/typescript/bin/tsc --init

Modify compilerOptions within tsconfig.json as needed. Of note:

Tips

Prefer conditional types returning never to type constraints

type AgeOf_1<T extends { age: number }> = T["age"];
type AgeOf_2<T> = T extends { age: number } ? T["age"] : never;

type T01 = AgeOf_1<{ age: 21 }>;
type T02 = AgeOf_1<{}>; /* error! */

type T10 = AgeOf_2<{ age: 21 }>;
type T11 = AgeOf_2<{}>; /* never */

Frequently Asked Questions (FAQs)

How should I think about the extends keyword?

You can think of extends as effectively testing: “is the type to the left of extends assignable to the one on the right?”.

Why is there sometimes an empty export {} at the bottom of a .ts file?

TypeScript needs a top-level import or export to identify a file as a module; otherwise it is considered a script and uses the global namespace.

How do I map the values of an object to a new type?

Use:

type M = { [V in T[number]]: V }

When should I be using any vs. unknown?

Use any when you want to use the referred-to variable without type checking. Use unknown when you do not know the type of the variable (or do not want to enforce it) but still want to have type checking when you use it.

Generally: prefer using unknown.

Why is TypeScript inferring the type to be boolean when I don’t expect it?

It is possible the type operation is distributive and the result is true | false, which TypeScript will indicate is boolean.

TypeScript Terminology

binding / bound
the association of a specific object to this
falsy
the quality of an object that evaluates to false
hoisting
term used to provide a conceptual understanding of how variable and function declarations are put into memory during the compile phase; they can be thought of as being “hoisted” to the top of the file
key
an enumerable property of an object
own property
a property belonging to an object and not a member of its prototype chain
truthy
the quality of an object that evaluates to true
tuple
an array for which the length is fixed and each index is typed

Resources

typescript-study

README.md

# TypeScript Study

Personal study area for TypeScript 4.6

## quickstart

Run the TypeScript compiler `tsc` on all source files:

```
$ ./node_modules/typescript/bin/tsc --watch
```

See all configuration in `./tsconfig.json`

src/ReturnType.ts

/* ReturnType<T> usage */

type t0 = ReturnType<() => number>; /* t0 is `number` */

export {};

src/conditional-types.ts

/* conditional types */

/* prefer moving type constraint to a conditional type check to allow easier-to-use types */
type AgeOf_1<T extends { age: number }> = T["age"];
type AgeOf_2<T> = T extends { age: number } ? T["age"] : never;

type T01 = AgeOf_1<{ age: 21 }>;
type T02 = AgeOf_1<{}>; /* error! */

type T10 = AgeOf_2<{ age: 21 }>;
type T11 = AgeOf_2<{}>; /* never */

export {};

src/distributive-conditional-types.ts

/* distributive conditional types */

/* conditional types distribute over union types. take for example:  */
type MyExclude<T, U> = T extends U ? never : T;

/* when passed a union type, we get: */
type NoC = MyExclude<"a" | "b" | "c", "c">; /* "a" | b" */

/* this works (effectively) via:
 *
 * type NoC = MyExclude<"a" | "b" | "c", "c">;
 * type NoC = MyExclude<"a", "c"> | MyExclude<"b", "c"> | MyExclude<"c", "c">;
 * type NoC = "a" extends "c" ? never : "a" | "b" extends "c" ? never : "b" | "c" extends "c" ? never : "c" ;
 * type NoC = "a" | "b" | never; */

export {};

src/generics.ts

/* generics */

/* rationale */

/* consider the identity function, implemented using `any`: */
const identityAny = (arg: any) => {
  return arg;
};

/* this does not get us type information of the argument we passed */
const a = identityAny("hello"); /* type: any */
const b = identityAny(100); /* type: any */

/* we could implement identity functions for all types */
const identityString = (arg: string) => {
  return arg;
};

const identityNumber = (arg: number) => {
  return arg;
};

const c = identityString("hello"); /* type: string */
const d = identityNumber(100); /* type: number */

/* but this requires a lot of redundant code! */

/* generic type parameters are type variables that we can use to specify types
in our function */
const identity = <T>(arg: T) => {
  return arg;
};

/* our first invocations pass the type of `arg` as an argument to the type
parameter */
const e = identity<string>("hello"); /* type: string */
const f = identity<number>(100); /* type: number */

/* but TypeScript can actually infer what the type parameter is based on the
value of `arg`, so we can omit: */
const g = identity("hello"); /* type: string */
const h = identity(100); /* type: number */

/* how to think about type parameters */

/* generic type parameters are type variables you declare as "inputs" that must
have corresponding use as "outputs" either as argument types or return types */
const gen1 = <T, U>(arg1: T, arg2: U) => {};
const gen2 = <T, U>(arg1: T, arg2: U): T => {
  return arg1;
};

export {};

src/indexed-access-types.ts

/* AKA Lookup Types */

type Foo = {
  kind: string;
  utility: number;
};

type T1 = Foo["kind"]; /* type: string */

/* indexed access also appears to be distributive: */
const tuple = ["a", "b", "c"] as const;

/* here `T[number]` distributes over keys that are of type `number` */
type Unionize<T extends readonly unknown[]> = T[number];

type U = Unionize<typeof tuple>; /* type: "a" | "b" | "c" */

export {};

src/intersection-types.ts

/* intersection types */

type T0 = { age: number };
type T1 = { name: string };

const foo = (x: T0 & T1) => {
  return `${x.name}: ${x.age}`;
};

export {};

src/keyof.ts

/* keyof type operator - index type query */

type Person = {
  name: string;
  age: number;
};

/* keyof takes an object type T and returns the string literal or numeric
literal union of T's keys (enumerable properties) */
type K1 = keyof Person; /* "name" | "age" */

export {};

src/literal-types.ts

/* literal types */

/* consider the following: */
var varString = "varString";
let letString = "letString";
const constString = "constString";

type V = typeof varString; /* string */
type L = typeof letString; /* string */
type C = typeof constString; /* "constString" */

/* `C` is typed with the string literal "constString"  */

/* this is useful for situations where you want specific values to be passed: */
const move = (direction: "north" | "east" | "south" | "west") => {
  return;
};

move("south"); /* OK */
move("northwest"); /* not OK */

/* the same can be done for number, with numeric literals: */
const randSorter = (): -1 | 0 | 1 => {
  const v = Math.random();
  if (v < 0.333) {
    return -1;
  } else if (0.333 <= v && v < 0.66) {
    return 0;
  } else {
    return 1;
  }
};

export {};

src/mapped-types.ts

/* mapped types */

type Person = {
  name: string;
  age: number;
};

/* let's imagine we want to create a type that has the same fields as Person,
but all are optional */

/* one attempt could be to recreate it with optional fields: */
type PartialPerson1 = {
  name?: string;
  age?: number;
};

/* but we can do better—the following protects against changes in Person
needing to cascade to PartialPerson. It uses the `keyof` operator to construct
a mapped type (`PartialPerson2`) from `Person` */
type PartialPerson2 = {
  [P in keyof Person]?: Person[P];
};

/* in fact, this is so common there is already a built-in type we can use */
type PartialPerson3 = Partial<Person>;

/* let's look a little deeper at mapped types and indexed accessed types */
const tuple = ["foo", "bar", 1, 2] as const;

/* if we just use `keyof T`, this will go through every key (enumerable
property), which for an array will the numbers reflective of the index */
type A<T extends readonly (string | number)[]> = {
  [P in keyof T]: P;
};

type APrime = A<typeof tuple>; /* readonly ["0", "1", "2", "3"] */

/* if we want to get the values referred to by those keys, we have to use an
indexed-access type T[number] to iterate through */
type B<T extends readonly (string | number)[]> = {
  [P in T[number]]: P;
};

type BPrime = B<typeof tuple>; /* { foo: "foo"; bar: "bar"; 1: 1; 2: 2; } */

/* use of `as` to remap keys (TypeScript 4.1 onward): */

type T0<U> = {
  [Key in keyof U as Key]: Key;
};

type T1<U, V> = {
  [Key in keyof U as Key extends V ? never : Key]: Key;
};

type T2<U extends { countryCode: string }> = {
  [Key in U as Key["countryCode"]]: Key;
};

type T3<U> = {
  [Key in keyof U as `get${Capitalize<string & Key>}`]: Key;
};

export {};

src/promise.ts

/* promise usage */

/* cannot specify the type of value `reject` takes / returns as the rejected
value. more info: https://stackoverflow.com/a/50071254/19982010 */
const p0 = new Promise<number>((resolve, reject) => {
  const rand = Math.floor(Math.random() * 10);
  if (rand < 5) {
    resolve(rand);
  } else {
    reject(`${rand} is greater than 4!`);
  }
  return "this return does nothing";
});

src/reduce.ts

/* reduce */

/* typescript type reduce. properly type reduce */

/* never[] */
const arr1 = [1, 2, 3].reduce((memo, n) => {
  return memo;
}, []);

/* number[] */
const arr2 = [1, 2, 3].reduce<number[]>((memo, n) => {
  return memo;
}, []);

/* number[] */
const arr3 = [1, 2, 3].reduce((memo, n) => {
  return memo;
}, <number[]>[]);

/* arr4 is inferred as number[], but memo is inferred as never[] */
const arr4: number[] = [1, 2, 3].reduce((memo, n) => {
  return memo;
}, []);

export {};

src/the-typescript-handbook/ch1-the-basics.ts

/* Chapter 1: The Basics */

export {};

src/the-typescript-handbook/ch2-everyday-types.ts

/* Chapter 2: Everyday Types */

export {};

src/the-typescript-handbook/ch3-narrowing.ts

/* Chapter 3: Narrowing */

export {};

src/the-typescript-handbook/ch4-more-on-functions.ts

/* Chapter 4: More on Functions */

/* ======Function Type Expressions====== */

/* a function with one parameter, named a , of type string, that doesn't have a
return value" */
function greeter(fn: (a: string) => void) {
  fn("Hello, World");
}

function printToConsole(s: string) {
  console.log(s);
}

greeter(printToConsole);

/* can type alias function signatures */
type GreeterFunction = (a: string) => void;
function greeter2(fn: GreeterFunction) {
  greeter(fn);
}

/* ======Call Signatures====== */
type DescribableFunction = {
  description: string;
  /* note the slightly different function signature syntax */
  (a: number): boolean;
};

/* ======Construct Signatures====== */
type SomeObject = {};
type SomeConstructor = {
  new (a: string): SomeObject;
};

/* note: "constructor" is sometimes abbreviated "ctor" */
function fn(ctor: SomeConstructor) {
  return new ctor("hello world");
}

interface CallOrConstruct {
  new (a: string): Date;
  (n?: number): number;
}

/* ======Generic Functions====== */
/* consider: */
function first(arr: any[]) {
  return arr[0];
}

function firstGeneric<T>(arr: T[]): T | undefined {
  return arr[0];
}

function firstGenericWithInference<T>(arr: T[]) {
  return arr[0];
}

/* x1 is inferred as type `any` */
const x1 = first(["a", "b"]);
/* x2 is inferred as type `string` */
const x2 = firstGeneric(["a", "b"]);

/* all inferred type `any` */
const x3a = first([]);
const x3b = firstGeneric([]);
const x3c = firstGenericWithInference([]);

const emptyNumberArr: number[] = [];
/* inferred type `any` */
const x4a = first(emptyNumberArr);
/* inferred type `number` (why not (number | undefined)?) */
const x4b = firstGeneric(emptyNumberArr);
/* inferred type `number` */
const x4c = firstGenericWithInference(emptyNumberArr);

/* multiple type parameters supported: */
const mapDemo = <T, U>(arr: T[], fn: (n: T) => U): U[] => {
  return arr.map(fn);
};

/* powers is inferred as type `number[]` */
const powers = mapDemo([1, 2, 3, 4], (n) => n * n);

const longer = <T extends { length: number }>(a: T, b: T): T => {
  if (a.length < b.length) {
    return b;
  } else {
    return a;
  }
};

const longerNumberArray = longer([1, 2, 3], [1, 2, 3, 4, 5]);
const longerStringArray = longer(["a", "b"], ["a"]);
/* T is inferred to be the union type: `(number[] | string[])`. I think this is
due to flow analysis of `longer`. */
const longerHmm = longer([1, 2, 3], ["a", "b"]);

const concatDemo = <T>(arr1: T[], arr2: T[]) => {
  return arr1.concat(arr2);
};

const c1 = concatDemo([1, 2], [3, 4]);
const c2 = concatDemo(["a", "b"], ["c", "d"]);
/* only works with specifying T as `number | string` */
const c3 = concatDemo<number | string>([1, 2], ["c", "d"]);

/* using fewer type parameters */
function filter1<T>(arr: T[], func: (arg: T) => boolean): T[] {
  return arr.filter(func);
}

/* in this case type `F` is declared as a type variable but only used on _one_
of the parameter values. generic type variables should be used to relate two or
more values across both the input and output parameters */
function filter2<T, F extends (arg: T) => boolean>(arr: T[], func: F): T[] {
  return arr.filter(func);
}

/* invoking when type can be inferred causes no difference */
const evens1a = filter1([1, 2, 3, 4], (n: number) => n % 2 === 0);
const evens2a = filter2([1, 2, 3, 4], (n: number) => n % 2 === 0);

/* but when TypeScript cannot infer the types, it must be specified (which is
excessively and unnecessarily verbose): */
/* TODO replace with example where TypeScript cannot infer the type */
const evens1b = <number[]>filter1([1, 2, 3, 4], (n: number) => n % 2 === 0);
const evens2b = <number[]>filter2([1, 2, 3, 4], (n: number) => n % 2 === 0);

/* optional parameters in callbacks */
const optFn = (n: number, fractionDigits?: number) => {
  /* `fractionDigits` has type (number | undefined) */
  console.log(n.toFixed(fractionDigits));
};

/* all OK: */
optFn(Math.PI);
optFn(Math.PI, 1);
optFn(Math.PI, 2);

/* could have also specified a default: */
const defFn = (n: number, fractionDigits = 4) => {
  console.log(n.toFixed(fractionDigits));
};

/* still OK: */
defFn(Math.PI); /* uses 4 fraction digits */
defFn(Math.PI, 1);
defFn(Math.PI, 2);

/* problem: we want `callback(a)` and `callback(a, i)` to both work  */
function myForEach1(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i);
  }
}

function myForEach2(arr: any[], callback: (arg: any, index?: number) => void) {
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i]);
  }
}

myForEach1([1, 2, 3], (a) => console.log(a));
myForEach1([1, 2, 3], (a, i) => {
  /* error TS2532: Object is possibly 'undefined'. */
  /* @ts-ignore */
  console.log(a, i.toFixed());
});

/* OK */
myForEach2([1, 2, 3], (a) => console.log(a));

myForEach2([1, 2, 3], (a, i) => {
  /* error TS2532: Object is possibly 'undefined'. */
  /* @ts-ignore */
  console.log(a, i.toFixed());
});

/* function overloads */

/* the following two function signatures are considered `overload signatures`: */
function makeDate(timestamp: number): Date;
function makeDate(m: number, d: number, y: number): Date;

/* the following function signature is considered an `implementation signature`
that is compatible with the above overload signatures _but cannot be called
directly_ */
function makeDate(mOrTimestamp: number, d?: number, y?: number): Date {
  if (d !== undefined && y !== undefined) {
    return new Date(y, mOrTimestamp, d);
  } else {
    return new Date(mOrTimestamp);
  }
}

/* OK: matches overload 1 */
const d1 = makeDate(12345678);
/* OK: marches overload 2 */
const d2 = makeDate(5, 5, 5);

/* error TS2575: No overload expects 2 arguments, but overloads do exist that
expect either 1 or 3 arguments. */
/* @ts-ignore */
const d3 = makeDate(1, 3);

/* Note: we may think that the implementation signature, with its 2nd and 3rd
optional parameters, should accept the following, but we would be wrong—calling
code must match overload signatures and cannot call implementation signatures
directly. */

/* to recap: */

/* - implementation signature cannot be viewed (called) from the outside */
/* - implementation signature must be compatible with all overload signatures */
/* - always prefer parameters with union types instead of overloads when possible */

/* declaring `this` in a function */
type User = {
  id: number;
  admin: boolean;
  becomeAdmin: () => void;
};
const user1: User = {
  id: 123,
  admin: false,
  becomeAdmin: function () {
    this.admin = true;
  },
};

interface DB {
  filterUsers(filter: (this: User) => boolean): User[];
}

/* CONFUSION: implement DB interface, but I have no idea how to use the filter
function passed to `filterUsers` correctly */
const getDB = () => {
  return {
    filterUsers(filter: (this: User) => boolean): User[] {
      return [];
    },
  };
};

const db = getDB();
const admins = db.filterUsers(function (this: User) {
  return this.admin;
});

/* important type: `void`. represents the return value of functions which don't
return a value */
const fVoid1 = () => {};
const fVoid2 = () => {
  return;
};

/* anything contextually typed w/ `void` retains that type  */
const fVoid3: () => void = () => {
  return "My value may be a string, but I am typed as void";
};

/* both typed as `void` */
const rf1 = fVoid1();
const rf2 = fVoid2();
const rf3 = fVoid3();

/* TODO check me */
if (rf1 === undefined) {
  console.log("I am typed as `void` but my value is `undefined`");
}

/* important type: `object`. refers to any value that is not a primitive */

/* important type: `unknown`. TODO find use case examples */

/* important type: `never`. useful for sussing out incomplete switch statements */

/* important type: `Function`. TODO find use case examples */

/* rest paramters and spread syntax */
const multiplyNumbers = (multi: number, ...nums: number[]) => {
  return nums.map((n) => n * multi);
};

multiplyNumbers(5, 1, 2, 3); /* 5, 10, 15 */
multiplyNumbers(5, ...[1, 2, 3]); /* 5, 10, 15 */

/* parameter destructuring */

/* error TS7031: Binding element 'b' implicitly has an 'any' type. */
/* @ts-ignore */
const pDest1 = ({ a, b }) => {
  console.log(a, b);
};

const pDest2 = ({ a, b }: { a: string; b: string }) => {
  console.log(a, b);
};

pDest1({ a: "a", b: "b" }); /* a: any, b: any */
pDest2({ a: "a", b: "b" }); /* a: string, b: string */

export {};

src/the-typescript-handbook/ch5-object-types.ts

/* Chapter 5: Object Types */

/* properties can be marked `readonly` */
const roFoo: { readonly a: string } = { a: "foo" };

/* error TS2540: Cannot assign to 'a' because it is a read-only property. */
/* @ts-ignore */
roFoo.a = "bar";

/* but that does not imply immutability: */
const roBar: { readonly ro: { bar: string } } = { ro: { bar: "bar" } };
roBar.ro.bar = "baz";

/* index signatures */
const numberLookup: { [index: string]: number } = {};
numberLookup["zero"] = 0;
numberLookup["one"] = 1;

/* note: `foo.someProperty` is also accessible as `foo["someProperty]` */

/* error TS2322: Type 'string' is not assignable to type 'number'. */
/* @ts-ignore */
numberLookup.doesntWork = "must be a number";

/* note: creating an index signature does not automatically promote or convert
an object to an array */

/* extending types */

interface Colorful {
  colorName: string;
}

interface Circle {
  radius: number;
}

interface ColorfulCircle1 extends Colorful, Circle {}

type ColorfulCircle2 = Colorful & Circle;

/* TODO: explain how to choose which. docs say based on conflict resolution but
quick check using types with overlapping property names doesn't actually
produce anything useful */

/* tuple types */
/* normal, optional, and rest elements are all supported */
type Tuple1 = [string, boolean?, ...number[]];

const workWithTuple = (t: Tuple1) => {
  /* index access works */
  const a = t[0]; /* string */
  const b = t[1]; /* boolean | undefined */
  /* TODO verify that I am actually number | undefined */
  const c = t[2]; /* number | undefined */

  /* alternatively: array destructuring also works  */
  const [destructuredA, destructuredB, ...desctructuredC] = t;
};

/* tip: mark your tuple values as consts */
const p = [2, 3] as const;

export {};

src/the-typescript-handbook/ch6-creating-types-from-types.ts

/* Chapter 6: Creating Types From Types */

export {};

src/the-typescript-handbook/ch7-generics.ts

/* Chapter 7: Generics */

export {};

src/the-typescript-handbook/ch8-classes.ts

/* Chapter 8: Classes */

export {};

src/the-typescript-handbook/ch9-modules.ts

/* Chapter 9: Modules */

export {};

src/tips.ts

/* tips */

/* how to write a function using destructing assignment with an optional arg? */

const opt1 = ({ foo, bar }: { foo?: string; bar?: string }) => {};
opt1({}); /* does not work */

/* error: A binding pattern parameter cannot be optional in an implementation signature */
const opt2 = ({ foo, bar }?: { foo?: string; bar?: string }) => {};

const opt3 = ({ foo, bar }: { foo?: string; bar?: string } = {}) => {};
opt3(); /* works! */

export {};

src/tuple-types.ts

/* tuple types */

/* consider the following information encoded in an array: */
const f = ["north", 1]; /* type: (string | number)[] */

/* if we want to access a specific index of that information the type system
cannot really help us narrow down the type of the element we are requesting */
const f0 = f[0]; /* type: string | number */
const f1 = f[1]; /* type: string | number */
const f2 = f[2]; /* type: string | number */

/* contrast that behavior with the use of a "tuple type" where the array type
info is supplied at instantiation */
const g: [string, number] = ["east", 2]; /* type: [string, number] */

const g0 = g[0]; /* type: string */
const g1 = g[1]; /* type: number */
const g2 =
  g[2]; /* error: Tuple type [string, number] of length '2' has no element at index '2' */

/* the spread operator can be used to define tuples with an unknown number of
elements, in any position: */
type T = [...string[], number, boolean];
type U = [string, ...number[], boolean];
type V = [string, number, ...boolean[]];

/* additionally, the spread operator works on generic type variables: */
type MyConcat<T extends unknown[], U extends unknown[]> = [...T, ...U];

type W = MyConcat<
  ["1", 2],
  [boolean, boolean]
>; /* ["1", 2, boolean, boolean] */

export {};

src/type-assertions.ts

/* type assertions */

enum Directions {
  North,
  East,
  South,
  West,
}

/* const assertions */

/* can only apply const assertions to references to enum members, or string,
number, boolean, array, or object literals */

/* TODO */
const possibleValuesEnum: Directions.East | Directions.West = Directions.East;

const possibleValuesString1 = "GET" as const;
const possibleValuesString2 = "GET" as "GET";

const possibleValuesNumber1 = 42 as const;
const possibleValuesNumber2 = 42 as 42;

/* not very useful, I guess */
const possibleValuesBool = true as const;
const possibleValuesBool2 = true as true;

const possibleValuesArray = ["yes", "no"] as const;

const possibleValuesObj = { url: "https://example.com" } as const;

/* cannot use const assertions on dynamic */
const lookup = new Map();
lookup.set("y", "yes");
lookup.set("n", "no");

/* error TS1355: A 'const' assertions can only be applied to references to enum
members, or string, number, boolean, array, or object literals. */
/* @ts-ignore */
const possibleValues2 = lookup.values() as const;

/* example use case: */
const nato1 = ["alfa", "bravo", "charlie"];

const letterFromWord1 = (word: string) => {
  /* implementation using `nato1` */
};

/* not resistant to misspellings */
letterFromWord1("alpha");

const nato2 = ["alfa", "bravo", "charlie"] as const;

/* type index syntax */
const letterFromWord2 = (word: typeof nato2[number]) => {
  /* implementation using `nato2` */
};

/* now we are resistant to misspellings */

/* error TS2345: Argument of type '"alpha"' is not assignable to parameter of
type '"alfa" | "bravo" | "charlie"'. */
/* @ts-ignore */
letterFromWord2("alpha");

/* typeof */

const foo: string | string[] = ["foo"];

/* error TS2367: This condition will always return 'false' since the types
'"string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object"
| "function"' and '"string[]"' have no overlap */
/* @ts-ignore */
if (typeof foo === "string[]") {
}

/* gotcha */
console.log(typeof null === "object"); /* true */

export {};

src/typeof.ts

/* typeof usage */

/* `typeof` in JavaScript is used on values */
const foo = "foo";
typeof foo; /* string */

/* `typeof` in TypeScript is used on types */
type t = typeof foo;
/* `t` is of type `string` */

export {};

src/union-types.ts

/* union types */

type StringOrNumber = string | number;

/* valid */
const s: StringOrNumber = "hello";
const n: StringOrNumber = 1;

/* error */
const a: StringOrNumber = ["whoops"];

/* using type narrowing */
const narrow = (x: string | number[]) => {
  if (typeof x === "string") {
    /* able to access to all of `string` properties */
    return x.charAt(0);
  }

  /* able to access to all of `number[]` properties */
  return x.lastIndexOf(0);
};

/* using overlapping properties */
const overlap = (x: string | number[]) => {
  /* only able to access overlapping properties */
  return x.slice(0, 2);
};

export {};

tsconfig.json

{
  "compilerOptions": {
    /* Visit https://aka.ms/tsconfig.json to read more about this file */

    /* Projects */
    // "incremental": true,                              /* Enable incremental compilation */
    // "composite": true,                                /* Enable constraints that allow a TypeScript project to be used with project references. */
    // "tsBuildInfoFile": "./",                          /* Specify the folder for .tsbuildinfo incremental compilation files. */
    // "disableSourceOfProjectReferenceRedirect": true,  /* Disable preferring source files instead of declaration files when referencing composite projects */
    // "disableSolutionSearching": true,                 /* Opt a project out of multi-project reference checking when editing. */
    // "disableReferencedProjectLoad": true,             /* Reduce the number of projects loaded automatically by TypeScript. */

    /* Language and Environment */
    "target": "es2016",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    // "lib": [],                                        /* Specify a set of bundled library declaration files that describe the target runtime environment. */
    // "jsx": "preserve",                                /* Specify what JSX code is generated. */
    // "experimentalDecorators": true,                   /* Enable experimental support for TC39 stage 2 draft decorators. */
    // "emitDecoratorMetadata": true,                    /* Emit design-type metadata for decorated declarations in source files. */
    // "jsxFactory": "",                                 /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
    // "jsxFragmentFactory": "",                         /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
    // "jsxImportSource": "",                            /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
    // "reactNamespace": "",                             /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
    // "noLib": true,                                    /* Disable including any library files, including the default lib.d.ts. */
    // "useDefineForClassFields": true,                  /* Emit ECMAScript-standard-compliant class fields. */

    /* Modules */
    "module": "commonjs",                                /* Specify what module code is generated. */
    "rootDir": "./src/",                                 /* Specify the root folder within your source files. */
    // "moduleResolution": "node",                       /* Specify how TypeScript looks up a file from a given module specifier. */
    // "baseUrl": "./",                                  /* Specify the base directory to resolve non-relative module names. */
    // "paths": {},                                      /* Specify a set of entries that re-map imports to additional lookup locations. */
    // "rootDirs": [],                                   /* Allow multiple folders to be treated as one when resolving modules. */
    // "typeRoots": [],                                  /* Specify multiple folders that act like `./node_modules/@types`. */
    // "types": [],                                      /* Specify type package names to be included without being referenced in a source file. */
    // "allowUmdGlobalAccess": true,                     /* Allow accessing UMD globals from modules. */
    // "resolveJsonModule": true,                        /* Enable importing .json files */
    // "noResolve": true,                                /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */

    /* JavaScript Support */
    // "allowJs": true,                                  /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
    // "checkJs": true,                                  /* Enable error reporting in type-checked JavaScript files. */
    // "maxNodeModuleJsDepth": 1,                        /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */

    /* Emit */
    // "declaration": true,                              /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
    // "declarationMap": true,                           /* Create sourcemaps for d.ts files. */
    // "emitDeclarationOnly": true,                      /* Only output d.ts files and not JavaScript files. */
    // "sourceMap": true,                                /* Create source map files for emitted JavaScript files. */
    // "outFile": "./",                                  /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
    // "outDir": "./",                                   /* Specify an output folder for all emitted files. */
    // "removeComments": true,                           /* Disable emitting comments. */
    "noEmit": true,                                      /* Disable emitting files from a compilation. */
    // "importHelpers": true,                            /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
    // "importsNotUsedAsValues": "remove",               /* Specify emit/checking behavior for imports that are only used for types */
    // "downlevelIteration": true,                       /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
    // "sourceRoot": "",                                 /* Specify the root path for debuggers to find the reference source code. */
    // "mapRoot": "",                                    /* Specify the location where debugger should locate map files instead of generated locations. */
    // "inlineSourceMap": true,                          /* Include sourcemap files inside the emitted JavaScript. */
    // "inlineSources": true,                            /* Include source code in the sourcemaps inside the emitted JavaScript. */
    // "emitBOM": true,                                  /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
    // "newLine": "crlf",                                /* Set the newline character for emitting files. */
    // "stripInternal": true,                            /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
    // "noEmitHelpers": true,                            /* Disable generating custom helper functions like `__extends` in compiled output. */
    // "noEmitOnError": true,                            /* Disable emitting files if any type checking errors are reported. */
    // "preserveConstEnums": true,                       /* Disable erasing `const enum` declarations in generated code. */
    // "declarationDir": "./",                           /* Specify the output directory for generated declaration files. */
    // "preserveValueImports": true,                     /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */

    /* Interop Constraints */
    // "isolatedModules": true,                          /* Ensure that each file can be safely transpiled without relying on other imports. */
    // "allowSyntheticDefaultImports": true,             /* Allow 'import x from y' when a module doesn't have a default export. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
    // "preserveSymlinks": true,                         /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */

    /* Type Checking */
    "strict": true,                                      /* Enable all strict type-checking options. */
    // "noImplicitAny": true,                            /* Enable error reporting for expressions and declarations with an implied `any` type.. */
    // "strictNullChecks": true,                         /* When type checking, take into account `null` and `undefined`. */
    // "strictFunctionTypes": true,                      /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
    // "strictBindCallApply": true,                      /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
    // "strictPropertyInitialization": true,             /* Check for class properties that are declared but not set in the constructor. */
    // "noImplicitThis": true,                           /* Enable error reporting when `this` is given the type `any`. */
    // "useUnknownInCatchVariables": true,               /* Type catch clause variables as 'unknown' instead of 'any'. */
    // "alwaysStrict": true,                             /* Ensure 'use strict' is always emitted. */
    // "noUnusedLocals": true,                           /* Enable error reporting when a local variables aren't read. */
    // "noUnusedParameters": true,                       /* Raise an error when a function parameter isn't read */
    // "exactOptionalPropertyTypes": true,               /* Interpret optional property types as written, rather than adding 'undefined'. */
    // "noImplicitReturns": true,                        /* Enable error reporting for codepaths that do not explicitly return in a function. */
    // "noFallthroughCasesInSwitch": true,               /* Enable error reporting for fallthrough cases in switch statements. */
    // "noUncheckedIndexedAccess": true,                 /* Include 'undefined' in index signature results */
    // "noImplicitOverride": true,                       /* Ensure overriding members in derived classes are marked with an override modifier. */
    // "noPropertyAccessFromIndexSignature": true,       /* Enforces using indexed accessors for keys declared using an indexed type */
    // "allowUnusedLabels": true,                        /* Disable error reporting for unused labels. */
    // "allowUnreachableCode": true,                     /* Disable error reporting for unreachable code. */

    /* Completeness */
    // "skipDefaultLibCheck": true,                      /* Skip type checking .d.ts files that are included with TypeScript. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}