Skip to content

Instantly share code, notes, and snippets.

@ronjouch
Last active November 18, 2022 19:16
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ronjouch/60f94ce7bafd15cfa74381611767f1b6 to your computer and use it in GitHub Desktop.
Save ronjouch/60f94ce7bafd15cfa74381611767f1b6 to your computer and use it in GitHub Desktop.
TypeScript Montréal meetup, 2011-11-17: TS @ Unito, A potpourri of various cool Node & TS thingies

TS Montréal meetup, 2011-11-17: TS @ Unito, A potpourri of various cool Node & TS thingies

A quick word about Unito

  • https://unito.io/
  • “That's all you do? I'll write it myself in a weekend hack”
    • 2-way-sync, corner cases, formatting, webhooks, ratelimits, multisyncs, .......
  • At it for soon a decade 🚜

TypeScript at Unito!

  • 7 years old codebase
  • Started with TypeScript 1.6
  • A couple dozen TS repos: libs, long-running apps, short-running apps
  • Used to be a Chrome extension 🤯! Now runs in AWS { EC2, Lambdas }
  • Backend all TS, Frontend all JS
  • A pretty darn gud work culture, and lots to do! Work with us! https://unito.io/careers/

TL;DR: say you have a useful function:

function getAwesome() {
  return 'zombocom'
}

And say you have a wrapper function, e.g. a (terrible) ratelimiter:

async function ratelimit(someFunction) {
  await sleep(1000)
  return someFunction()
}

WITHOUT a decorator:

function getAwesomeUNDECORATED() {
  return 'zombocom'
}
export function getAwesome = ratelimit(getAwesomeUNDECORATED)

→ Blergh, separated from declaration, maybe dozens of lines later. Not obvious to maintainers, prone to forgetting about the decorator.

But WITH a decorator:

@ratelimit
function getAwesome() {
  return 'zombocom'
}

→ Win! Decorating is done immediately near the place of declaration, visible & obvious to maintainers.

We use decorators for all kinds of stuff at Unito!

Caching!

https://github.com/unitoio/cachette

@CacheClient.cached(120)
public async function getBoard(
  boardId: string,
  options: GetItemOptions,
): Promise<Container> { /* ... */ } 

→ Determines a cache key for you, setting TTL to cache for 2 min, all at time of fn declaration

Error normalization!

@error.normalizer(normalizeAppError)
async function getTask(taskId: string) { /* ... */ }

→ Again, normalization visible at time of decl, and using a local normalizer tailored to this class's getTask

On-demand profiling!

@ddProfile
public async function getContainers() { /* ... */ }

→ Easy-to-use profiling in our logging & metrics tool

ORM! (not ours, TypeORM) ***

export class Anomaly extends BaseEntity {

  @PrimaryGeneratedColumn({ type: 'bigint' }) // telling the ORM how to db-model this field
  id: number;

  @Index('Anomaly-linkId-idx') // telling the ORM how to db-model this field
  @Column({ nullable: false }) // telling the ORM how to db-model this field
  linkId: string;

  // ...
}

→ Declaring a SQL schema just with decorators annotations

Final caution word about decorators

TS decorators are NOT standard JS / ECMAScript. JS decorators are (might?) be on the way to TC39 standardization. Potential breaking change upcoming needing migration.

→ Use at your own risk, after evaluating you're comfy with it

Making REST: tsoa & oazapfts

Suppose a working backend for which you want a REST API. tsoa gives you decorators & tooling to produce:

  1. TS code for your REST API
  2. OpenAPI schema (yaml/json) & docs
@Route('containers') // decorator exposed by tsoa
export class TaskController extends Controller {

  @Get('{containerId}/tasks') // decorator exposed by tsoa
  public async getTasks(
    containerId: string,
    @Query() modifiedSince: number = 0, // decorator exposed by tsoa
    @Query() options?: GetTaskOptions, // decorator exposed by tsoa
  ): Promise<Item[]> { /* ... */ }
}

→ Will cause tsoa to produce TS for GET /containers/<container_id>/tasks , with type validation 🙂. → And a standard openapi yaml/json schema file, usable with tons of tooling

Then! What about generating a client? oazapfts to the rescue → Avoids tedious & error-prone client/server alignment!

Helping tsc help you!

Unlike other typed langs, the types of TS variables as understood by TypeScript change through program flow / TS type inferrence! Use it to your benefit! Don’t write TS as if you were writing Rust! Both are good but different.

  • Ad-hoc: simple if statements & assert()
const accessKeyId = assumeRoleResp.Credentials?.AccessKeyId;
// string | undefined
assert(accessKeyId, `Failed to assume role ${roleArn}.`);
// string!
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

Generally less sloppy typing

type Worker = { // kinda not so great :-/
  name: 'string';
  employment: 'employee' | 'freelancer';
  monthlyPay: number | undefined; // undefined for freelancers, whose pay is per-contract
}

vs.

type Worker = // safer!
{ // Employee
  name: 'string';
  employment: 'employee';
  monthlyPay: number;
  } |
{ // Freelancer
  name: 'string';
  employment: 'freelancer';
  // no monthlyPay here
}

→ Writing worker.monthlyPay with 2. will cause TS to force you to narrow your use case, whereas 1. may let you shoot yourself in the foot!

TS in your JS! Editor! CI!

Benefits of TS-in-JS aren't limited to your editor integrating it! To typecheck a JS thingie in CI, just call tsc on it with --allowJs --checkJs ! Not as complete as full-blown TS, but still pretty gud.

tsc
  --allowJs # yayyyyyyyyyyyyyyy
  --checkJs # yayyyyyyyyyyyyyyy
  --noEmit
  --module es2022
  --target es2022
  --moduleResolution node16
  --strict
  yourjavascriptfile.mjs

{Get, Set}ters & Proxy

Useful to e.g. keep a derived field up-to-date. Proxy is a bit more powerful.

Lovely good typing done by some libs

Some libs do better typing than others! Find them and use them!

Example 1: Mocking with "Sinon"

beforeEach(() => {
  sinon.stub(Clock, 'getDate').returns(new Date('2018-10-01T09:00:00.000Z'));
  // okay, works as expected
});

beforeEach(() => {
  sinon.stub(Clock, 'getDate').returns(true);
  // Wheeeeeeeeeeee!
  // 🧨 Argument of type 'boolean' is not assignable to parameter of type 'Date' 🧨\n});

Example 2: Mongoose: similar stuff, forcing you to declare a model, which then in ORM fashion means Mongoose can immediately return well-typed stuff from the DB 🙂.

→ General point: writing TS isn't just typing well your stuff! There's a whole ecosystem, and some of it does typing better than others! Find the good stuff and benefit from it!

YOU WANT A PACKAGE-LOCK

(╯°□°)╯︵ ┻━┻ YES REALLY, DO IT NOW (ノಠ益ಠ)ノ彡︵ ┻━┻︵ ┻━┻ https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json

  1. To limit exposure to regressions! You don't want your build to break when obscure-deep-dep ships a regression!
  2. To limit exposure to supply-chain attacks https://blog.sonatype.com/npm-project-used-by-millions-hijacked-in-supply-chain-attack https://eslint.org/blog/2018/07/postmortem-for-malicious-package-publishes/

More generally, for reproducibility!

Iterators & Generators!

Vidy nice to traverse a structure / graph in fancy dynamic at-runtime ways!

export class PendingChangeIterator {
  public next(): PendingChange { /* ... */ }
  public done: boolean
  // You respected the "iterator protocol"!
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol
}
// → your iterator is now usable by a for...of loop!
for (const pendingChange of pendingChangeIterator) {
  await syncPendingChange(pendingChange)
}

→ See also Generator functions. We use them to traverse graphs

We use them for:

  1. Faster compilation! (esp. incremental)
  2. Guaranteed isolation
D↓              tests      sdk-tests
E↓               / \         /
P↓   scripts    /   service /
E↓      \      /   / \     /
N↓      mainproject   \  sdk
D↓                 \   \ /
S↓                  types

Say you want to ship { sdk + types } as a separate npm package...

  • In "regular TS" (no project mode), no guarantee that someone won't accidentally introduce a sdk→mainproject dep! 😬
  • In project mode, you're protected against that 🙂

By the way, to investigate TS compile time perf, ts / wiki / Performance is good. Usual compiler caveat: don't degrade code to micro-optimize everything, let the compiler do its job, etc. But sometimes you don't have a choice.

Awesome tooling 💖

[{--}]❔ Questions to youuuuu ❔[{--}]

  • NoSQL MongoDB up/down migrations, anyone?
  • npm audit is tedious. What do you do with it?
  • Dependency creep: do you attempt to avoid it? How?
  • Abandoned/unpatched open-source dependencies: what to do
  • Non-node runtimes (Deno, Bun): anyone using them in prod?
  • Worker threads! C/C++! Calling other languages: anyone using them in prod?
  • The mayhem of types incompat when you have different versions of the same library, and npm link / peerDependencies
  • Monorepos! Do you do them? EDIT got good feedback about https://lerna.js.org/ and https://nx.dev/ , thx!
  • The delicate balancing act of features vs. quality/bug fixes

Thank team for the topics ideas! Thank youuu for joining us here!

(ノ◕ヮ◕)ノ*:・✧ *********** ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ ROARING, ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ THUNDEROUS, ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ DEAFENING, ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ APPLAUSE, ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ AND ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ QUESTIONS ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ *********** ✧・: *ヽ(◕ヮ◕ヽ)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment