Programming TypeScript - KeynesYouDigIt/Knowledge GitHub Wiki

Introduction

Type Safety: Using types to prevent programs from doing invalid things

TypeScript: A 10_000 Foot View

Typechecker: A special program that verifies that your code is typesafe

Your code goes TS -> TS AST -> Typechecker checks AST -> JS

Type system: A set of rules that a typechecker uses to assign types to your program.

"You see this value here? Here's it's type!"

Each project should include:

.tsconfig.json

  • Generate with npx tsc --init
// .tsconfig.json

{
  "compilerOptions": {
    "lib": ["es2015"], // What exists in the environment?
    "module": "commonjs", // What module system are you using?
    "outDir": "dist", // Where should your .js code go?
    "sourceMap": true,
    "strict": true, // Require all of your code to have types
    "target": "es2015" // Which JS version should your code compile to?
  },
  "include": [ // Where are your .ts files?
    "src"
  ]
}

eslint.json

  • Generate with npx eslint --init

index.ts

All About Types

Type: A set of values and the things you can do with them. Knowing a type lets you know valid values and valid operations.

Example: A Boolean is the set of all booleans (true and false), and the operations you can perform on them (||, &&, !)

  • Annotate: Add the type to a value
  • Constrain: Restrict a parameter to a value
  • Bounds: Upper and lower values for something
  • Assignable: An argument being compatible with a type
  • Type literal: A type that represents a single value and nothing else. Good for maximizing type safety.
  • Structural typing: A style of programming where you just care that an object has certain properties, and not what its name is (nominal typing). Also called "Duck Typing" in some languages.
  • Definite assignment: Making sure that a value has been assigned before its used.
  • Index signature: [key: T]: U - "All keys of type T must have values of type U."

Types

any: Set of all values, which you can do anything with. Use a last resort. Top type. unknown: Set of all values, but can only do comparison. If you check the type with if (typeof thing === 'number'), you can do type-specific operations. Will get Better than any. boolean: Two values, you can compare them or negate them. You can annotate it as being a specific boolean (type literal), otherwise can always be inferred. number: All numbers, NaN, and Infinity. All numeric operations. Also can always be inferred. bigint: New, very large numbers. string: Set of all strings, can concat, slice, etc. Also can always be inferred. symbol: Can be annoted as unique. Rare. Just like a type literal. object: Must specify the shape of the object. Can be inferred. Inferring doesn't do literal values since values are reassignable. Avoid emtpy objects, TS won't infer them correctly. Arrays- T[] or Array<T>, eg string[]: Explicitly type. Typescript won't narrow your types if you create an array with an empty literal, it will just add each type to its inference. Keep arrays homogenous, or you have to type check them every time you want to do something with one of them. Tuples- [T...], eg. [string, boolean]: Mixed types. Have to declare up front or TS will assume its an array. null: One value. Absence of value, eg. an error prevented something from being calculated. It's the subtype of all types except never, which means that every type is nullable, which means you always have to check if something exists first. strictNullChecks prevents this. undefined: One value. Never assigned. void: No explicit return value (eg. console.log) never: No values. A function that never stops running, an exception throwing. The "bottom type," a proposition that's always false.

Index signatures

Structure: [key: keyType]: valueType

let a {
  b: number
  c?: string // Optional
  readOnly d: number // `const` for properties
  readOnlyArray<string> // Optional syntax
  [key: number]: boolean // An unknown number of properties that have numbers for keys and booleans for values
  [someString: string]: string // A
}

Type Aliases

You can use the type keyword to assign a type to a value. Makes your types more expressive. You can switch an alias for its type interchangeably. You can't declare a type twice. They're scoped and shadowed.

type Age = number
const kyle = {
  age: Age
}

Unions and Intersections

Union is combined types, intersection is shared types. Unions are more common.

type Cat = { name: string, meows: boolean }
type Dog = { name: string, barks: boolean }
type CatOrDogOrBoth = Cat | Dog
type CatAndDog = Cat & Dog

A functions returned value might be something like string | boolean

Enums

Fixed range of values. Don't use them in code that you publish, or it might conflict with someone else's. They are difficult to use safely, and can be avoided.

enum Language = {
  English = 100,
  Spanish = 200,
  Russian // Infers 301
}

Language.English
Language[200]

Or (safer):

const enum Language = {
  English,
  Spanish,
  Russian
}

Language.English

Functions

  • TypeScript generally can't infer parameters, but can often infer return type
  • Parameters can be optional (must go last) or have defaults (don't need to be explicitly typed)
  • arguments and function are unsafe

Definitions

  • Formal Parameter - Parameter
  • Actual Parameter - Argument
  • Type Level Code - Valid TypeScript, but not valid JavaScript on its own
  • Value Level Code - Valid JavaScript
  • Contextual Typing - Using a function signature type to get the types of parameters and return values
  • Overloaded function - A function with multiple call signatures
  • Concrete Type - A definite type, like boolean, Date[], { a: number } | { b: string }, (numbers: number[]) => number
  • Generic Type Parameter - A placeholder type used to enforce a type-level constraint in multiple places. Also called a polymorphic type parameter.
  • Upper Bound - A type that another type must have somewhere in its hierarchy
function add(a: number, b: number): number {
  return a + b
}

function add(...numbers: number[]) {
  return numbers.reduce((sum, number) => sum += number)
}

type someObject = {
  someKey: "someValue"
  someOptionalKey?: "someOtherValue"
}

function someFunction(): someObject {
  // Whatever
}

function typeThis(this: someObject): void {
}

// Typing a function signature
// When you do this, you don't need also annotate the parameters in the function definition ("contextual typing")
type Greeting = (message: string) => void

// Overloaded function signature
type Reserve = {
  (from: Date, to: Date, destination: string): Reservation
  (from: Date, destination: string): Reservation
}
// When you implement this, you need to combine it into one type:
let reserve: Reserve = (from: Date, toOrDestination: Date | String, destination?: string) => {
  // And then check to see which one it is:
  if (toOrDestination instanceof Date && destination !== undefined){
    // it's `to`
  }
}

Generics

Generics are placeholders for type. You can indicate that multiple things need to be the same type, without specifying what they are ahead of time.

type Filter = {
  <T>(array: T[], fn: (item: T) => boolean): T[]
}
type Map = {
  <T, U>(array: T[], fn: (item: T) => U): U[]
}

type Whatever = <T = string>(T): boolean // default value
  • Generics are like placeholder types
  • <T, U, V> are the TS conventions, but they can be anything, including descriptive words
  • Concrete types are bound at compile time, and are compiled differently for each instance
  • Generic types generally inferred well, but may need to be explicitly bound as well (eg. Promises resolution values)

Generics can be scoped two different ways:

// Per signature
type Filter = {
  <T>(array: T[], fn: (item: T) => boolean): T[]
}
// or
type Filter = <T>(array: T[], fn: (item: T) => boolean): T[]
let filter: Filter = //

// For all signatures
type Filter<T> = {
  (array: T[], fn: (item: T) => boolean): T[]
}
// or
type Filter<T> = (array: T[], fn: (item: T) => boolean): T[]
let filter: Filter<number> = //

You can also use generics in type aliases, and pass those like parameters:

type SomeType<T> = {
  someKey: T
  someOtherKey: string
}

type SomeOtherType<T> = {
  someKey: SomeType<T>
  someOtherKey: boolean
}

type SomeConcreteType = SomeOtherType<string>

You can "bound" generics to at least be some concrete type (including joins and intersections), which preserves the original type while still bounding it.

type NodeMapper = <T extends TreeNode>(item: T[]): T
type NodeMapper = <T extends TreeNode & OtherNode>(item: T[]): T

Classes and Interfaces

  • Adds access modifiers, readonly
  • Abstract classes can include overrideable methods
  • Interfaces are lighter and just define a shape and can be extended, and can't include type expressions
  • Interfaces can be declared multiple times and will merge
    • Interfaces can take in generics, but all generics must have the same type and name
  • this can be a return type
  • Classes are structurally typed- that means it doesn't check to see if the names are the same, only if the types are assignable
  • Types and values have different namespaces, except for classes and enums, which uses the same for both
  • Classes generate types for both the Class itself as well as the constructor
  • Classes can have generics, as can methods
class Piece<K, V> implements SomeInterface, SomeOtherInterface {
  constructor(
    private readonly color: Color,
    file: File,
    rank: Rank,
  ){
    this.position = new Position(file, rank)
  }
  something = (K): V => {
  }
}

Mixins

  • A mixin is just a function that takes in a class constructor and returns a class constructor
// Declares a new type that takes in a generic
// It is equal to a constructor that takes in any arguments, and returns that generic
type ClassConstructor<T> = new(...args: any[]) => T

//the function addDebug, which declares a generic, which gives a value for T
function addDebug
  <C extends ClassConstructor
    <{ getDebugValue(): object }>
  >
  // Takes in a class as an argument, it needs to be a class constructor
  (Class: C) {
    // Returns an anonymous class that extends the target class and has a debug method
    return class extends Class {
      debug(){
        let Name = Class.constructor.name
        let value = this.getDebugValue()
        return `${Name}(${JSON.stringify(value)})`
      }
    }
  }

const User = new addDebug(SomeClass)
const someUser = new User("a", 1, false)
someUser.debug()

Decorators

Wrap a class and return a new one with new functionality. Not quite ready for primetime yet.

function serializable<
  T extends ClassConstructor<{
    getValue(): Payload
  }>
>(Constructor: T) {
  return class extends Constructor {
    serialize() {
      return this.getValue().toString()
    }
  }
}

@serializable
class APIPayload {
  getValue(): Payload {
    //
  }
}

Factory Pattern

  • Union types increase safety
let Shoe {
  create(type: 'boot' | 'sneaker' | 'dress'): Shoe {
    switch(type){
      case 'boot': return new Boot
      case 'sneaker: return new Sneaker
      case 'dress: return new DressShoe
    }
  }
}

Builder Pattern

class RequestBuilder {
  private url: string | null = null
  private data: object | null = null

  setURL(url: string): this {
    this.url = url
    return this
  }
  setData(data: object): this {
    this.data = data
    return this
  }
}

new RequestBuilder()
  .setUrl("google.com")
  .setData({})

Advanced Types

  • If something is a subtype, you can use it anywhere the supertype is required
    • never is a subtype of everything, and a supertype of nothing
    • any is a subtype of everything
  • Figuring out this relationship is trickier once generics get involved.

For purposes of the book:

  • A <: B means "A is a B or a subtype"
  • A >: B means "A is a B or a supertype"

4 types of variance:

  • Invariance: You want exactly a T
  • Covariance: You want <: a T
  • Contravariance: You want >: a T
  • Bivariance: You want either >: or <: a T

Typescript always expects you to use types that are the same or subtypes of the ones you declare ("covariance"), except function parameters, which expect the same or supertypes of what you declare ("contravariance").

Function Variance

A function is a subtype of another function if:

  • Same or lower arity
  • Its this type isn't specified or is >:
  • Each of its parameters are >:
  • Its return type is <:

The reason the parameters need to be broader for it to be subtype is that if you're going to swap the subtype function anywhere a supertype function is expected, you can't have things in the function that the supertype might be able to pass in.

Assignability

For non-enums, something is assignable if it's <:, or the type is any. For enums, something is assignable if it's in the enum, or if it's a number and one of the members of the enum is a number. This is kind of dangerous, so enums are best avoided.

Type Widening

If you use const, the type is the literal value. If you use let, it's the base type for the value. You can explitly annotate to narrow it. Reassigning a type from const to let widens it. null and undefined are widened to any, but given definite types when they leave the scope (like by invoking a function). You can opt out of type widening (recursively!) with as const.

let a = 1 // number
const b = 1 // 1
let c: 1 | 2 = 1 // 1 | 2
let d = { a: { b: "c" }} as const // { readonly a: { readonly b: "c" }}

Excess Property Checking

TS will catch if you add a property to an object that wasn't on the type.

Refinement

TS does "flow-based type inference", which means it follows the logic of your app and narrows things like union types to their specific types.

Discriminated Unions

Flow-based type inference will pick out specific types, but has limits to its logic:

type a = {a: number, b: string}
type b = {a: array, b: boolean}
type aOrB = a | b

function (someparameter: aOrB){
  if (typeof someParameter.a === "number"){
    someParameter.a // number
    someParameter.b // string | boolean, uh oh
  }
}

To fix this, add an extra artificial property that discriminates between them:

type a = {type: "a", a: number, b: string}
type b = {type: "b", a: array, b: boolean}
type aOrB = a | b

function (someparameter: aOrB){
  if (someParameter.type == "a"){
    someParameter.a // number
    someParameter.b // string
  }
}

Totality

Also called "exhaustiveness checking." TypeScript checks for unhandled cases and code paths that don't return values.

Keying In Operator

Pull out types from (for example), an API response:

type Owner = {
  name: "Kyle",
  dogs: [{
    name: "Bixby",
    breed: "Chow"
  }]
}

type Dogs = Owner["dogs"]
type Dog = Owner["dogs"][number] // Keys into an array

Dot notation doesn't work for this.

keyof Operator

Make a union of all of an object's keys:

type someObject = {
  a: 1,
  b: 2,
}

type someObjectKeys = keyof someObject // "a" | "b"

Mapped Types

These allow you to map through the keys of an object and transform their types:

type Readonly<T> = {
  readonly [K in keyof T]: T[P]
}

type ReadOnlyDog = Readonly<Dog>

Record Types

These allow you to type keys and all values, for things like dictionary lookups.

type ProductID = string
type AvailabilityTypes = 'sold_out' | 'in_stock' | 'pre_order'

interface Availability {
  availability: AvailabilityTypes
  amount: number
}

const store: Record<ProductID, Availability> = {
  '0d3d8fhd': { availability: 'in_stock', amount: 23 },
  '0ea43bed': { availability: 'sold_out', amount: 0 },
  '6ea7fa3c': { availability: 'sold_out', amount: 0 },
}

Other mapped types

  • Partial<Object> - Makes every field optional
  • Required<Object> - Makes every field required
  • Readonly<Object> - Makes every field read only
  • Pick<Object, Keys> - Returns a subtype of the object with only the given keys

Companion Objects

Since types and variables have different namespaces, you can name an object and a type the same thing. It also lets you import then both at the same time:

// Currency.ts

type Currency = {}
class Currency extends Currency {
  static exchange(){}
}

export Currency Currency

// App.ts

import { Currency } from "./Currency"

const someMoney: Currency = {}
const exchangedCurrency = Currency.exchange("USD")

Improving Type Inference for Tuples

Ordinary tuples infer pretty wide:

let a = [1, true] // (number | boolean)[]

Make function that spreads the arguments, and they'll get typed by position:

function tuple<T extends unknown[]>(...types: T): T {
  return types
}

let a = tuple([1, true]) // [number, boolean]

User-Defined Type Guards

Type refinement only works within the scope you're in. So this won't work:

function isString(a: unknown): boolean {
  return typeof a === "string"
}

function someFunction(a: string | number){
  if (isString(a)){
    a.toUpperCase() // Error, can't call that on numbers
  }
}

To fix that, you can use the is keyword. This means that the return value is boolean, and the parameter you passed in should be refined. You can only use this with one parameter.

function isString(a: unknown): a is string {
  return typeof a === "string"
}

Conditional Types

Types with ternaries. If something is a subtype, assign to A, otherwise to B.

type IsString<T> = T extends string
  ? true
  : false

Distributive Conditionals

Distributive property applies.

(string | number) extends T ? A : B

// is the same as

(string extends T ? A : B) | (number extends T ? A : B)

The infer keyword

A way to do a generic parameter.

type SomeType<T> = T extends (infer U)[] ? U : T

Built-in Conditional Types

  • Exclude<T, U> - Get the types T that are not in U
  • Extract<T, U> - Get the types T that are assignable to U
  • NonNullable<T> - Exclude null and undefined from T
  • ReturnType<F> - Calculate a function's return type
  • InstanceType<C> - Get the instance type of a class constructor

Escape Hatches

Use rarely.

Type Assertions

Tell Typescript to assign a related type:

let someVariable: string | number = "hi"
someFunction(someVariable as string)
someFunction(someVariable as any) // Super unsafe

Non-Null Assertions

You can tell the typechecker that you're confident that something isn't null or undefined with !:

type Something = {
  id?: string
}

let something: Something = { id: 2 }

find(something.id!)

This is usually a sign that you need some refactoring to multiple types.

Definite Assignment Assertions

Tell the typechecker that something will definitely have a value before it's used:

let userId!: string

assignId()

userId.toUpperCase()

Simulating Nominal Types

A technique called "type branding":

type SpecificTypeOfID = string & { readonly brand: unique symbol }
function SpecificTypeOfID(id: string){
  return id as SpecificTypeOfID
}

Rare to use, but keeps different kinds of IDs from getting accidentally interchanged in a large system and causing logic errors.

Handling Errors

Ways to handle errors:

  • Return null
    • Doesn't tell you why something failed
    • It's awkward to check for null every time
  • Throw exceptions
    • Tells you why something failed
    • Types can't ensure that all exceptions are accounted for
  • Return exceptions and make return type a union of the happy path and each exception
    • Tells you why something failed
    • Forces developers to handle exceptions
    • Verbose
  • Option type
    • Allow you to chain operations if one of them might fail
    • Doesn't tell you why something failed
    • Not interoperable with code that doesn't use monads

Returning errors

function somethingThatCouldError(): boolean | SomeException | SomeOtherException {
  //
}

const result = somethingThatCouldError()
if (result instanceof SomeException) // do something
if (result instanceof SomeOtherException) do something

Options

Instead of returning a value, you return a container that might have a value or might not. The container has some methods that let you chain an operation or return a value.

Examples:

  • Try
  • Either
  • Option / Maybe - What we're doing
interface Option<T> {
  flatMap<U>(someFunction: (value: T) => None): None
  flatMap<U>(someFunction: (value: T) => Option<U>): Option<U>
  getOrElse(value: T): T
}

// A successful operation that resulted in a value
class Some<T> implements Option<T> {
  constructor(private value: T){}

  // Overloaded signature
  flatMap<U>(someFunction: (value: T) => None<U>): None
  flatMap<U>(someFunction: (value: T) => Some<U>): Some<U>
  flatMap<U>(someFunction: (value: T) => Option<U>): Option<U> {
    return someFunction(this.value)
  }
  getOrElse(): T {
    return this.value
  }
}

// An operation that failed and doesn't have a value
class None implements Option<never> {
  flatMap<U>(): None{
    return this
  }
  getOrElse<U>(value: U): U {
    return value
  }
}

// Overloaded signature
function Option<T>(value: null | undefined): None
function Option<T>(value: T): Some<T>
function Option<T>(value: T): Option<T> {
  return (value == null)
    ? new None
    : new Some(value)
}

let result = Option(someValue)
  .flatMap(someFunction)
  .flatMap(someOtherFunction)
  .getOrElse("Whoops")

Asynchronus Programming, Concurrency, and Parallelism

Promise types: Promise<ResolvedValue, ErrorType>

You can handle emitters by overloading the signature:

type RedisClient = {
  on(event: "ready", f: () => void): void
  on(event: "error", f: (e: Error) => void): void
  on(
    event: "reconnecting",
    f: (params: { attempt: number, delay: number }) => void
  ): void
}

By having the event type narrowly typed, we can further narrow the function signature. You can also do this with mapped types:

type Events = {
  ready: void
  error: Error
  reconnecting: { attempt: number, delay: number }
}

type RedisClient = {
  on<E extends keyof Events>(
    event: E,
    f: (arg: Events[E]) => void
  ): void
  emit<E extends keyof Events>(
    event: E,
    arg: Event[E]
  ): void
}

This is common, terse, non-repetitive, and good.

Frontend and Backend Frameworks

  • Add the support you need to your tsconfig.json (eg. "compilerOptions": { "lib": ["dom", "es2015"] })
    • This doesn't add any extra code, just tells the compiler what things it should let go
  • TSX = JSX with TS support
    • Enable with compilerOptions: { "jsx": "react" } (compiles to a .js file)
    • Can also do "react-natve" and "preserve" (typecheck, but leave the file alone)
  • TS ensures that JSX is well-formed and the required parts are being passed in
  • You can't type SQL, but you can type your ORM calls

React:

import React from "react"

// Functional
type Props = {
  isDisabled?: boolean
  someOtherProp: string
}

export default function SomeComponent(props: props){
  return ()
}

// Class
type Props = {
  isDisabled?: boolean
  someOtherProp: string
}
type State = {
  isActive: boolean
}

export default class SomeComponent extends React.Component<Props, State>{
}

Namespaces.Modules

History of modularization:

  • 1995 - Globals, IIFEs
  • 2004 - Dojo (had a module loader)
  • 2005 - YUI (had a module loader)
  • 2008 - AMD
  • 2009 - LABjs (had a module loader)
  • 2009 - CommonJS
  • 2011 - Browserify
  • 2015 - ES2015 import/export

ES2015 modules can be statically analyzed- CommonJS modules cannot.

You can dynamically import a module with import(), which returns a promise. This is only allowed in TS with the "module": "esnext" } mode. You can pass any expression in, but you'll lose type-safety if it's not a string. To get around this, you can manually annotate the import:

import { locale } from "./locales/locale-us" // Type

async function something(){
  const locale: typeof locale = await import(somePath)
}

You can allow default exports from CommonJS modules with { "compilerOptions": { "esModuleInterop": true }}

TS decides whether to parse a .ts file in module mode or script mode based on whether or not there are any import or export statements.

TypeScript has a namespaces feature built-in, but should be avoided in favor of modules.

Interoperating With JavaScript

Type Declarations

  • A file that ends with .d.ts
  • Can only have types, no values
  • Only for code that's visible to consumers (otherwise you could just use the TS code directly). You can compile your code and include type declarations, and now other TS projects don't need to also compile your code. They also provide hinting for editors.
  • Can be generated with tsc -d some-file.ts
  • Can declare that types exist somewhere (eg. globals, imported modules)
  • Can be use to generate "ambient types" that don't need to be imported every time
    • Useful for models
  • Use types.ts for ambient types in a TS project, filename.d.ts for JS files
declare let process: {
  env: {
    NODE_ENV: 'development' | 'production'
  }
}

declare module 'some-module' {
  export type MyType = number
  let myDefaultExport = MyType
  export default myDefaultExport
}
declare module 'unsafe-module'
declare module 'something-with-wildcards*'

//

import someModule from "some-module" // Typed!
import unsafeModule from "unsafe-module" // All types are `any`
import something from "something-wild-wildcards-like-this"

Steps to Migrate from JS to TS

  1. Add TSC to your project
  • With { "compilerOptions": { "allowJs": true}}
  • It won't typecheck, but it will transpile it
  1. Start type-checking existing JS code
  • With { "compilerOptions": { "checkJs": true}}
  • You may also want to allow implicit anys with { "compilerOptions": { "noImplicitAny": false}}
  • If this generates too many errors, you can throw // @ts-check at the top of a file to check just that file
  • You can ignore a particularly noisy file with // @ts-nocheck
  • You can typecheck individual functions with JSDoc
  1. Migrate JS to TS, one file at a time
  • Either do each file one by one, or mass-rename and use toggling strict mode to find errors
  1. Install type declarations for your dependencies
  • If the types are included by the authors, use them
  • Otherwise, install the community types from DefinitelyTyped (npm i @types/module-name)
  • You can whitelist an untyped module once with // @ts-ignore which will type it as any
  • You can whitelist an untyped module always by making an empty ambient module declaration
  • You can create an accurate ambient module declaration
  • You can contribute your ambient module declaration to DefinitelyTyped
  1. Turn on strict mode
  • Disable JS
/**
  * @param word { string } An input string to convert
  * @returns { string } The converted string
  */
export function someFunction(word){
  return word.toLowerCase()
}

Type Lookup Steps for JS files

  1. Look for a sibling .d.ts file with the same name as the JS file
  2. Otherwise try to infer the types and use JSDoc annotations
  3. Otherwise, treat the whole module as any

Building and Running TypeScript

TS artifacts:

  • .js files
  • .js.map - Source maps
    • Links each line of your generated code to the appropriate line of its TS source
  • .d.ts - Type declarations
    • Lets others use your types
  • .d.ts.map - Declaration maps
    • Speeds up compilation when you're using references

tsconfig.json

  • Compile targets: Transpilation is done for many things, but not regex flags, object getters/setters, and BigInts.
  • Lib: A list of types that TS can bring in (eg. dom, es2015, es2015.array.includes)
  • Include source maps (not emitted by default) for easier debugging

Project References

As the app gets bigger, you can TS compile parts of it separately with project references:

{
  "compilerOptions": {
    "composite": true, // Part of a larger project
    "declaration": true, // Generate declarations
    "declarationMap": true, // Build source maps for the declarations
    "rootDir": "." // Compiled relative to this folder
  },
  "include": [
    "./**/*.ts"
  ],
  "references": [{
    "path": "../myReferencedProject", // A dependency of this project
    "prepend": true
  }]
}

Make a root tsconfig.json that brings together all your projects that aren't referenced by anything else (or they'll go in twice) and references them:

{
  "files": [],
  "references": [{
    "path": "./SomeProject"
  },{
    "path": "./SomeOtherProject"
  }]
}

Build everything with tsc --build or tsc -b. You can also make json files that extend other files to share settings.

.gitignore your generated artifacts, and .npmignore your source code. Put a types key in your package.json that indicates where the generated types are.

Appendix: Type Operators

Type operator Syntax Use on
Type query typeof, instanceof Any type
Keys keyof Objects
Property lookup O[K] Objects
Mapped type [K in O] Objects
Add/Sub Modifier +/- Objects
Read-only Modifier readonly Objects, Arrays, Tuples
Optional Modifier ? Objects, Arrays, Tuples, Function Params
Nonnull Assertion ! Nullable types
Generic Default = Generic types
Type assertion as, <> Any type
Type guard is Function return types

Appendix: Type Utilities

Type utilities Used on Description
ConstructorParameters Class constructors Tuple of parameter types
Exclude Union types Exclude a type from another type
Extract Union types Select a subtype that's assignable to another type
InstanceType Class constructors The type you get from newing a constructor
NonNullable Nullable types Exclude null and undefined from a type
Parameters Function types Tuple of a function's parameter types
Partial Object types Make all properties in an object optional
Pick Ojbect types A subtype of an object type, with a subset of its keys
Readonly Arrays, objects, tuples Make all properties or keys readonly
ReadonlyArray Any type Make an immutable array of the type
Record Object types Map from a key type to a value type
Required Object types Make all properties in an object required
ReturnType Function types A function's return type
⚠️ **GitHub.com Fallback** ⚠️