Skip to content

TypeScript

Created: 2020-03-25 10:04:45 -0700 Modified: 2023-12-14 10:48:19 -0800

  • Installing
    • They suggest globally installing it, but that’s somewhat opinionated since some people suggest only installing it as a dev dependency (which is what I’m going with). Either way, it’s “npm install typescript” with an optional “-g” flag if you want it to be globally accessible.
      • Note that by installing locally, you will need to run “npx tsc” or “yarn tsc” instead of just “tsc”.
  • Usage with Visual Studio Code
    • There’s first-class support, so you don’t need to install anything.
    • If you ever run into a problem, open the command palette and choose “TypeScript: Restart TS server”.
    • Note that when using JavaScript, you’ll just get Intellisense support, not proper type-checking.
  • Compiling
    • tsc filename
    • Note that it will still produce the file even if there are errors (reference)
  • Configuring (compiler option reference)
    • Note: to quickly configure a bunch of boilerplate (linting, formatting, pre-commit hooks, GitHub actions, etc.), you can just run “pnpm create @hideoo/app”. Make sure to look in the various files (package.json, README, license, etc.) for references to HiDeoo, and consider looking in the .github folder for any actions that you may want to change/delete.
    • tsc —init
    • This will produce tsconfig.json file which will let you customize the options.
    • Some good starting options to consider:
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
  • Facts
    • [10:03] strothj: TypeScript is structurally typed. Which means, as long as an object meets the required shape of an interface/type then it is that type. Other languages like Java/C# are nominal, you have to explicitly say they are something
  • Quickly testing (AKA playground) (reference)
    • There’s a TypeScript playground that you can use. Note that some features are apparently still buggy, but usually, it’s fine. For example, even just this correct code will give you a bug: const name = “hi”; (it says “name” is used by “input.ts”).
    • If you ever want to “trick” the playground by acting like a type is only known at runtime, you can just add any condition, even if it’s constant, e.g. “true ? typeA : typeB”;
  • Linting (reference)
  • Usage with React
  • Guidelines
    • Have at least “noImplicitAny” (or “strict”) set to true in the configuration. Most people try to at least make sure that “noImplicitAny” is satisfied
    • Avoid “any” as a type wherever possible; it sort of undermines TypeScript’s philosophy of adding type-checking.

Consider the following code snippets:

interface Crayon { color: string }
function printColor(crayon: Crayon) {
console.log(crayon.color);
}
// CODE SNIPPET #1
// ------------------------
let crayon = {} as Crayon;
crayon.color = "red";
// ------------------------
// CODE SNIPPET #2
// ------------------------
let crayon: Crayon = {};
crayon.color = "red";
// ------------------------
printColor(crayon);

In snippet #1, you are asserting that “crayon” is of type “Crayon” even though it doesn’t match due to lacking a color. On the next line, we define a color, but if we’d omitted that, then our printColor function could eventually receive a crayon that has no color.

In snippet #2, we don’t use type assertions, we just use regular typing. That makes TypeScript check at compile time whether “crayon” has the expected properties of the interface, which it doesn’t, so it will say “Property ‘color’ is missing in type ’{}’ but required in type ‘Crayon’.“.

Type assertions will still verify that properties match the expected type. For example, you can’t say “crayon.color = true” without getting an error. In general though, it doesn’t seem like they’re very useful since it can completely undermine static typing as shown above (where printColor can’t rely on a property existing).

[11:32] zaridu: there are valid uses for as casts, namely some functions like Object.keys() that return a string array but should return am array of string literals that belong to be the object’s property names. AFAIK there is no way to avoid using as casts for this

Overall conclusion: try to avoid “as” unless you’re forced to use it. You’re basically telling TypeScript that you know more than it does in some particular case.

Exclamation mark (AKA non-null assertion operator)

Section titled Exclamation mark (AKA non-null assertion operator)

Type assertion to eliminate null/undefined (reference) - use the exclamation point like so:

function foo(name: string | null): string {
return name!.charAt(0);
}
console.log(foo(null));

Being part of type assertions, this is essentially you saying to TypeScript that you know better, so this will error at runtime since you are trying to call “charAt(0)” on null. However, if you didn’t have the exclamation mark, then you’d get a compile-time error saying something like “null doesn’t have charAt”.

  • By annotating a constructor’s parameters with a visibility modifier like “private” or “public”, a member variable is automatically created for you (reference).
  • Tuples (reference)
    • You can define an array with a fixed number of elements using a tuple, e.g. let x: [string, number];
  • Enums (reference, reference2)
    • These are friendly names for 0-indexed numeric values, although you can set the starting value (reference).
    • You can get the friendly name (as a string) at compile time by using square brackets, e.g. Colors[Colors.Green] as opposed to simply using Colors.Green (which would be a number).
    • Enum values can be constant (e.g. 1, 1 << 2, Read | Write) or computed (e.g. “123”.length) (reference)
    • An enum can become a union if all of its values are literal (reference).
    • At runtime, a numeric enum has both directions for lookup (AKA “reverse mappings” (reference):

enum Fruits {

Apple,

Banana

}

Compiling that into JavaScript and running the result gives this: { ‘0’: ‘Apple’, ‘1’: ‘Banana’, Apple: 0, Banana: 1 }

  • At compile time, you can get a list of all possible enum values (reference)
enum LogLevel {
ERROR, WARN, INFO, DEBUG
}
/**
* This is equivalent to:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
  • If you don’t want any part of an enum to show up during runtime, then use a const enum (reference). They can’t have computed members, which is what allows them to be inlined at compile time.

  • Object (reference)

  • Never (reference)

    • You would use this for functions that never return a valid type, e.g. a function that always throws an Error or one that contains an infinite loop.
  • Destructuring with types (reference)

    • You put the types after all of the destructured properties
      • Array: const [first, second]: [number, number] = [1, 2];
      • Object: const { name, age }: { name: string, age: number } = person;
  • Interface (reference)

    • Interfaces are just a way of having a reusable type for structural typing (i.e. duck typing). This just means that whatever property is of the interface type has to match the interface, but it could still have more properties.
      • Structural subtyping is as opposed to nominal typing (great specific example in this section):
        • [09:09] Monadic_bind: yeah so like if you have type Foo { name: string} and type Bar { name: string }, you can’t treat a Foo as a Bar in a nominal typing system
        • [09:10] Monadic_bind: “nominal” basically means “name-based”
      • Object literals get special treatment in that they do check for excess properties (reference). For example:
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log();
}
let myObj = {size: 10, label: "Size 10 Object"};
// This is fine because we're passing a variable, not an object literal
printLabel(myObj);
// This is NOT fine because we're directly passing an object literal, which will detect that "size" is an excess property
printLabel({size: 10, label: "Size 10 Object"});
// This is also an error because we assigned a type to our object literal.
let otherObj:LabeledValue = {size: 10, label: "Size 10 Object"};
  • If you want to get around this, use type assertions.

  • Also, if you have a private or protected member, then those are treated specially and the properties in two types being checked have to originate from the same types (reference).

  • Optional properties (reference): append a question mark after the name, e.g. “color?: string;“

  • Readonly properties (must be specified when the object is first created) (reference): prepend “readonly”, e.g. “readonly color: string;“.

    • There’s also ReadonlyArray that’s analogous to Array.
    • readonly is for properties. const is for variables.
    • You can’t put “readonly” into a function definition like this:
function printNum(readonly x: number) { // BAD (no "readonly" allowed here)
console.log(x);
}
  • Function types (reference)
    • Syntax: you have to declare parameter names, types, and the return type, e.g.
interface SearchFunc {
(source: string, subString: string): boolean;
}

However, the names of parameters aren’t actually checked.

  • Indexable types (reference)

    • For arrays and objects that can have indexes (e.g. someArray[0], someObject[‘name’], someObject.name), you can specify indexable types.
  • Implementing an interface (reference)

    • Classes can say they implement an interface which would mean that they have to have the same properties/functions, but only publicly, and only on the instance side (e.g. you can’t have static properties like a constructor in the interface). There is a way to accomplish this, but it seems obtuse to me.
  • Extending interfaces (reference)

  • Classes (reference)

    • Visibility modifiers (reference)
      • Private: can only be accessed in the class
        • You can also put a ”#” into a variable name to mark it as private.
          • Remember that you have to refer to the variable by that name from then on.
          • This has runtime overhead since a check has to be done to ensure that the property is accessible
      • Protected: can only be accessed in the inheritance hierarchy
      • Public (default): can be accessed by anything
      • If you put a visibility modifier or "
      • " into a parameter in the constructor, it will be converted into a member variable.
    • Accessors (getters and setters) (reference)
    • Abstract classes (reference): use the abstract keyword
  • Functions (reference)

    • Default parameters can still exist and you wouldn’t need a question mark:
      • function buildName(firstName:string = “hello”)
    • Explicit “this” parameter (reference)
      • If you need a type for “this” in a function, then you can define it sort of like how Python does for classes by listing it as the first parameter with a type.
    • Function overloading (reference)
      • You define the signatures for each possible way of calling the function and/or returning values, and then you have the function implementation itself (which below has “any” as the return type, but you would use an interface or generics for the real implementation):
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// implementation here that may return a number or Object
}
  • You may see the arrow being used to declare return types, e.g. something like this:
const capitalizeName: (arg: string) => string = (name) => name.toUpperCase();

The first arrow (”=>”) is to indicate that our function’s type returns a string. The second arrow is to indicate our function returns the capitalized name.

  • You may see a colon being used if you specify the function’s type in an object literal (reference):
const capitalizeName: { (arg: string): string } = (name) => name.toUpperCase();
  • Generics (reference)
    • You can name the types anything you want, but T is generally the most common, and R may be used for the return type if it’s different.
    • Calling a generic function (reference):
      • Explicitly provide types: identity<string>(“Hello”)
      • Implicitly infer types: identity(“Hello”)
    • Arrays of a generic type (pick either one) (reference):
function loggingIdentity<T>(arg: T[]): T[] { /*code*/ }
function loggingIdentity<T>(arg: Array<T>): Array<T> { /*code*/ }
  • You can make interfaces themselves generic as well (reference):
interface GenericIdentityFn<T> {
(arg: T): T;
}
  • You can make classes generic (reference)

  • Generics can be used for factories (reference)

    • [11:45] Monadic_bind: Imagine you have a bunch of different types of game objects. You can make a generic “create<T extends GameObject>” function that, in addition to calling the constructor, also registers it to a list of game objects, assigns it a uuid, etc.
  • Type inference (reference)

    • Pro-tip: you can tell what a type something is by hovering over it in the playground or in your editor

  • Examples

    • Basic: let x = 5; // ← infers that it’s a number
    • Finding common types: let x = [0, 1, null]; // ← infers that it’s an Array<number | null>.
  • Be careful with type inference. Many times, it will identify “too common” of a type because it’s trying to strictly match against existing types.

  • Contextual typing (reference): this is when there’s a known structure for something like window.onmousedown; TypeScript knows that the event is a MouseEvent, so it will automatically infer the type. They don’t explicitly list in the handbook all functions that will have contextual typing, so I suppose you should just hover over the function that you write to see if it has it, and if not, add your own typings.

  • Type compatibility (reference)

    • This section mostly talks about how you can use type X instead of type Y in some cases (e.g. you can say that a Rhino is an Animal, but not vice versa). For functions, this has special semantics around optional/required parameters, return values, etc. For enums, you can use numbers and enums interchangeably. This same comparison continues for classes, generics, etc.
    • Terminology
      • Sound: if type safety can be verified at compile time, then the program is sound. TypeScript does allow unsound behavior rarely in certain scenarios.
    • The arity of functions doesn’t have to be the same for you to change around types (reference):
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
  • The reason behind allowing this is because you’ll frequently write JavaScript that ignores some number of the parameters, e.g. Array.forEach provides 3 arguments to its callback, but you may only use one or two of them.

  • Advanced types (reference)

    • Intersection types (reference) - just like how you can say a type can be any of some set (e.g. number | boolean, AKA union typing), you can say that a type is all of some set (e.g. interfaceA & interfaceB). This is generally used for mixins.
    • Union types (reference) - this is what was mentioned in intersection types, e.g. a union could be number | boolean. When used with non-primitive types, you can only access members that are in both types. E.g. Fish | Bird could both access “layEggs()” function but not necessarily “fly()“.
    • Type guards and differentiating types (reference)
      • Basic overview
        • Suppose you have a union of Fish | Bird as in the example above and you want to see exactly which type it is at runtime. You could use type assertions:
if ((pet as Fish).swim) {
(pet as Fish).swim();
}
  • …but that’s tedious to keep typing out, so you can instead use a user-defined type guard, which is a function that returns a type predicate:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
  • Similarly, you can do something like this:
function isCommand(command: Command): command is Command {
return "data" in command && "execute" in command;
}
async function main() {
// This will never throw an error on trying to cast to Command, hence the need for isCommand
const command = (await import(somePath)) as Command;
if (isCommand(command)) {
// ...
}
}
  • “Type narrowing” is a term that means determining more about a particular type. For example, you may know that something is a Fish | Bird | Insect, so if you eventually find out that it can’t be an Insect, then you’ve narrowed that type.
  • You can’t necessarily differentiate between all kinds of types just because they look different at compile time; they have to look different at runtime. For example, suppose you have this:
enum Fruit {
Apple,
Orange,
}
enum Vegetable {
Broccoli,
Carrot,
}
type FruitOrVegetable = Fruit | Vegetable;
// ❌ This does not work!
function isFruit(food: FruitOrVegetable): food is Fruit {
return food === Fruit.Apple || food === Fruit.Orange;
}

The reason this doesn’t work is because both enums are numeric, so at runtime, all four choices get collapsed into either 0 or 1. TypeScript does know at compile time that “function juiceFruit(fruit: Fruit)” can’t take in a Vegetable.Carrot, but that’s lost at runtime. To fix this, you need some information that persists at runtime. For example, I would just use objects whose values are unique strings (by the way, objects vs. enums is discussed in the official docs):

const Fruits = {
Apple: "apple",
Orange: "orange",
} as const;
type Fruit = (typeof Fruits)[keyof typeof Fruits];
const Vegetables = {
Broccoli: "broccoli",
Carrot: "carrot",
} as const;
type Vegetable = (typeof Vegetables)[keyof typeof Vegetables];
type FruitOrVegetable = Fruit | Vegetable;
const allFruits = new Set(Object.values(Fruits));
function isFruit(food: FruitOrVegetable): food is Fruit {
return allFruits.has(food as Fruit);
}
  • Type predicates (reference) - type predicates let you say something like “pet is Fish” as the return value of a function, that way you can take your Fish | Bird, run it through the type predicate, and know that it’s a Fish at runtime. Here’s an example:
function isAxiosError(error: any): error is AxiosError {
return !!error.isAxiosError; // we rely on Axios-specific errors to have the "isAxiosError" property attached
}
  • Another example would be getting a JSON response back and being able to assert that it actually matches the JSON format that you expect rather than using a type assertion to say that you know better than TypeScript does.

  • A final example is something like Lodash’s functions for _.isString, _.isNumber, etc.

  • ”in” operator (reference) - this is like the duck typing that you may be used to in JavaScript, only you’ll get proper type-checking:

function move(pet: Fish | Bird) {
if ("swim" in pet) {
return pet.swim();
}
return pet.fly();
}
  • typeof type guards (reference)
    • Instead of using “variable is Type” for type guards, you can use “typeof”. However, this doesn’t work for custom types, only number, boolean, string, e.g.
if (typeof name === "string") {
return name.toUpperCase();
}
  • instanceof (reference)
    • Use instanceof sort of like typeof but with constructors. E.g. “person instanceof Manager”.
  • Nullable types (reference)
    • Remember that with the “—strictNullChecks” flag on, no type can be nullable by default, meaning something like “let bob:Person = null;” will be erroneous.
    • Optional access
      • Optional chaining (reference)
        • It’s the ”?.” operator, and it will short-circuit to undefined if what’s being optionally chained is null or undefined. For example:

let x = foo?.bar.baz();

  • When foo is undefined, the whole thing will be undefined. When it isn’t, the expression will evaluate to foo.bar.baz(). Note that this will still fail when “bar” or “baz” is undefined.

  • Optional element access (reference)

    • This is just like ”?.” but for accessing properties using square brackets, e.g. arr?.[0]
  • Optional call (reference)

    • Optionally call some function: log?.(“hello world”)
  • Note that with any of these, the ”?” only applies to what’s before it, so this will still cause an error: person?.age + functionThatDoesNotExist()

  • Nullish coalescing (reference): this is basically just like _.defaultTo from Lodash:

let x = foo ?? bar; // uses "foo" if it exists (i.e. not null or undefined), otherwise uses "bar".
  • Aliases (AKA the “type” keyword) (reference)
    • This is just a way to give a new name to a type, not to create a new type altogether. In essence, if you ever had a single-line interface, you could probably replace it with an alias, although there are apparently slight differences (reference), so I think a general guideline is still to prefer “interface” over “alias”. Examples of simple aliases:
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
  • The “declare” keyword (reference):
    • This is just a hint to the compiler that you already made the type elsewhere, so it won’t convert into any JavaScript.
  • Discriminated unions (AKA “tagged unions” or “algebraic data types”) (reference):
    • This is when you have multiple types that have a common property (AKA the “discriminant”), then you union all of those types and guard on that property. It’s a lot of words to show a simple concept which is at the reference link.
    • If you use a discriminated union, you need to check for exhaustiveness (reference), that way, when you add another type to your union, you don’t forget to check it. See the reference link for more details on how to do this.
  • Polymorphic “this” types (AKA “F-bounded polymorphism”) (reference)
    • This is really nice for the builder pattern where you want to return “this” after every function, that way you can do something like builder.addEars().addNose().whatever(). With F-bounded polymorphism, you can extend a class that does this and not have to make any modifications. You do this by using “this” as the return type:
public add(operand: number): this {
this.value += operand;
return this;
}
  • Index types (reference)
    • If you ever have a function like “pluck” that looks like this:
function pluck(o, propertyNames) {
return propertyNames.map((n) => o[n]);
}
  • …you might want to look at the reference link for how they use generics and “keyof” to address the need to only pluck valid properties from a particular object in a generic way.
  • ”keyof” is the index type query operator (reference):
interface Person {
name: string;
age: number;
location: string;
}
type K1 = keyof Person; // "name" | "age" | "location"
  • “indexed access operator” (reference) - this is something like T[K] when “K extends keyof T”, e.g.
function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
return o[propertyName]; // o[propertyName] is of type T[K]
}
  • Mapped types (reference)
    • A good term to know is homomorphism, which is when a mapping applies only to properties of type T and no other types. In general, the way you would tell if a TypeScript type is homomorphic is if it’s generic. Record is not homomorphic, but Readonly and Partial are.
    • This is a reference to the functional “.map” operation where you apply a function to every element in a collection. In this case, it’s for applying a function to each type, e.g. making all properties in an existing type readonly or optional:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}
  • Note that if you want to add a member to a type, you would do that with intersection types rather than just a partial, e.g.
type MyPartial<T> = {
[P in keyof T]?: T[P];
}

type PartialWithMember<T> = MyPartial<T> & { newMember: boolean };

interface HexCode {
hex: string;
}
type Color = 'red' | 'green' | 'blue';
const colorToHex: Record<Color, HexCode> = {
red: { hex: '#ff0000' },
green: { hex: '#00ff00' },
blue: { hex: '#0000ff' },
};
  • Pick - this is sort of like Lodash’s _.pick

  • You can “unwrap” mapped homomorphic types (reference)

  • You can conditionally map types (e.g. make something a string if another type is a boolean) (reference). It sounds like the reason to want this is because some JavaScript code would return different types based on the inputs, so you may want to conditionally handle that with more than just an intersection type. It also sounds like if you coded in TypeScript from the beginning of a codebase that you’d probably never use this.

  • Type inference in conditional types (reference)

    • I think that this is a feature that you’ll use as part of TypeScript built-ins, e.g. ReturnType (see Robin’s message below), but I don’t know how frequently you have to use this on your own.

[09:35] RobinMalfait: That ReturnType is pretty handy. E.g.: ReturnType<typeof setTimeout>; So that you know the return type of setTimeout. Because in the browser this is a number. In node this is NodeJS.Timer object IIRC.

  • Here’s a much more complete example from Robin showing where you’d use this.

  • The reference link has other examples of type inference in conditional types, e.g. NonNullable, Exclude, and Extract.

  • Symbols (reference)

    • In general, TypeScript symbols don’t really add anything onto JavaScript symbols, so if you don’t know JavaScript symbols, it’s worth reading that page. In short, they’re just constants that aren’t exactly strings but are used as identifiers for object properties.
  • Iterators and generators (reference)

    • Just like with symbols, this doesn’t really have anything TypeScript-specific in it.
  • Modules (reference)

    • Importing types (reference) - if you want to import just a type from another module, use “import type” instead of just “import”. This generates no JavaScript.
    • Ambient declarations are ones that don’t provide an implementation, e.g. a .d.ts file that just has types (reference).
      • Note that “.d.ts” files are sort of an intermediary between JavaScript and TypeScript. The “ideal” way of converting from JS → TS is to just add types directly in the code.
    • Module resolution (reference)
      • There isn’t too much on this page. Here’s what I thought was important:
        • If you don’t specify a file type (e.g. import * from “./foo”), then TypeScript will look for “./foo.ts” before looking for “./foo.d.ts”.
        • There’s a similar mechanism to Webpack’s aliases in the form of virtual directories (reference). This lets you alias paths so that you can do something like:

import messages from ‘./#{locale}/messages’

…instead of import messages from ‘./a/really/long/path/to/locale/messages’

This will convert to the JavaScript as though you’d typed the long path.

  • Namespaces (reference)
    • Apparently this is considered “old TypeScript”, so I don’t think it’s important to learn. Just use modules.
    • Namespaces are specific to TypeScript, i.e. they aren’t a JavaScript feature.
    • Namespaces can be defined across many files. They have somewhat strange comment-based syntax for doing this (e.g. ”/// <reference path=“Validation.ts” />”).
  • Declaration merging (reference)
    • This is when the compiler merges two separate declarations into a single name. Declarations are any of a namespace, a type, or a value (and you probably won’t use namespaces, so they’re basically just types and values). A value is something that gets emitted into JavaScript, whereas types are purely used by TypeScript.
    • It sounds like the only time you really want to use this is to modify a global declaration like “window”.
    • The simplest example is merging two interface declarations that have the same name (reference):
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};
  • JSX (reference)
    • With JavaScript, you can use JSX in a file whose extension is “.js”, but you can’t do the same with TypeScript due to angle-bracket type assertions (reference).
      • [10:50] HiDeoo: For example: “const t = <Person>tt;” (the bracket would mess up JSX), if you want the same assertion in TSX, you use “const t = tt as Person;“
      • [10:51] kingdaro_: as a convention, I would say to always use as since it works in both files
      • [10:52] HiDeoo: Yeah, as is the way to go, altho as shouldn’t be used a lot anw
    • There are several modes that you can use based on the rest of your build chain (reference), e.g. you can use preserve mode so that your JSX is untouched, then something like Babel can process it. All modes will still check types, otherwise they’d be pointless.
    • Intrinsic elements vs. value-based elements (reference): intrinsic elements would be something like a “div” or “img” in the DOM whereas value-based elements would be something like TabBar or SpecialButton.
      • They have a lot of guidance around this, but I think it really just boils down to the note farther down on the page, which is “use React’s typings” (reference).
    • Random notes
      • Use the utility types
        • [10:41] The_AwD: One thing that really took me a while to figure out was introducing props via HOCs and not having to define them optional. Utilty types are pretty handy for that
      • Check out Preact’s typings
  • Decorators (reference)
    • Decorators resolve to functions that are executed at runtime.
    • Decorators cannot be used in declaration files or other ambient contexts
    • A decorator factory is just a function that returns a function representing the decorator. By doing so, you can decorate with the factory, e.g. “@factory()” instead of “@method_the_factory_would_have_returned” (notice no parentheses). Factories are helpful if you want to parameterize the decorator, e.g. “@enumerable(false)“.
    • Decorator composition (reference)
      • You can put the decorators on the same line or on multiple lines. If you do “@f @g x”, it’s equivalent to f(g(x)).
    • Decorator evaluation (reference) - there’s an “order of operations” to decorators in a class: instance members → static members → constructor → class decorators
    • The rest of the docs mostly talk about the specific kinds of decorators, what their arguments would be, and any other caveats about them.
  • Mixins (reference)
    • Mixins are essentially a way to get composition (instead of inheritance) to combine multiple partial classes into a full one. This DigitalOcean resource has slightly more context around that.
    • Remember this quote from the declaration merging section on “disallowed merges”: “Currently, classes can not merge with other classes or with variables”. This essentially means that you can’t extend multiple classes at once since the merging is forbidden. For that reason, you would use mixins.
class Enemy extends Entity, IsVisible ← BAD
interface Enemy extends Entity, IsVisible ← GOOD

By going the interface route, you’ll only get the type-checking and not the implementations of the interfaces. This is why the documentation has you write an “applyMixins” function

  • Type-checking JavaScript files (reference)
    • If you’re not converting a JS codebase, then this section is pretty much meaningless. Some quick notes:
      • Types can be inferred from JSDoc-style comments
      • The rest of the documentation seems to talk about the specific rules that TypeScript applies to JavaScript files to follow JavaScript paradigms while still trying to add value, e.g. object literals are open-ended.

Sometimes, a library exposes a function without exposing the return type. Imagine something like:

returnsAnUnexposedType(): SecretType {
// Returns a SecretType
}

…you can still get the type yourself via ReturnType<ClassName["returnsAnUnexposedType"]>. You can also add something like Awaited to it if it should be wrapped in a Promise.

Lots of times, for well-established libraries, the types are exposed to callers and they’re just hard to find. E.g. I wanted the type for google.youtube("v3").liveChatMessages.list. Turns out I had to do this:

import { youtube_v3 } from "googleapis"
// Now use youtube_v3.Schema$LiveChatMessageListResponse

Installing types for existing packages

Section titled Installing types for existing packages
  • Use this site: https://microsoft.github.io/TypeSearch/
    • Install everything as a dev dependency even if you’re using Visual Studio Code, which has first-class support for TypeScript (meaning it will automatically locate these types in most cases (reference)), that way other developers on your team can be using whichever editor they prefer.

error instanceof SomeError not working

Section titled error instanceof SomeError not working

The issue is related to this in that there are likely two versions of SomeError. In my specific case when I hit this last, I was doing something like this:

  • I had a monorepo with bots and youtube-api packages.
  • The youtube-api package exported a StreamNoLongerLiveError
  • The bots package imported it via import { StreamNoLongerLiveError } from "../../youtube-api/src/StreamNoLongerLiveError.js"
  • The bots package had code like this:
    async function main() {
    try {
    await someIssueThatThrowsTheErrorIDefined()
    } catch (error) {
    console.log(error instanceof StreamNoLongerLiveError)
    }
    }

The problem is that this would log false even though I expected it to log true.

The reason why this was happening is that the StreamNoLongerLiveError had two versions:

  • One that the youtube-api package saw (this was the “real” version)
  • One that the bots package imported

This was due to the build process: the youtube-api package was being transpiled and then imported, whereas the bots package was running with tsx (which uses esbuild).

The fix in my particular case was to export the StreamNoLongerLiveError version from youtube-api:

  • Add this to youtube-api’s index.ts: export * from "./StreamNoLongerLiveError.js"
  • Import from bots using the new path: import { StreamNoLongerLiveError } from "youtube-api"

In general, if you see src in the path of an import across packages, you know there’s a problem.