- 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 🚜
- 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()
}
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.
@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!
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.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
@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
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
Suppose a working backend for which you want a REST API. tsoa gives you decorators & tooling to produce:
- TS code for your REST API
- 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!
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;
}
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!
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
Useful to e.g. keep a derived field up-to-date. Proxy is a bit more powerful.
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!
(╯°□°)╯︵ ┻━┻ YES REALLY, DO IT NOW (ノಠ益ಠ)ノ彡︵ ┻━┻︵ ┻━┻ https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json
- To limit exposure to regressions! You don't want your build to break when obscure-deep-dep ships a regression!
- 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!
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:
- Faster compilation! (esp. incremental)
- 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.
- tsc --watch & node (≥18) --watch
- node (≥18) built-in test module
- The TS release notes are excellently concise/detail balanced! Subscribe to them and read them!
- 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
(ノ◕ヮ◕)ノ*:・✧ *********** ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ ROARING, ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ THUNDEROUS, ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ DEAFENING, ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ APPLAUSE, ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ AND ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ QUESTIONS ✧・: ヽ(◕ヮ◕ヽ) (ノ◕ヮ◕)ノ:・✧ *********** ✧・: *ヽ(◕ヮ◕ヽ)