<style type="text/css"> .reveal { font-size: 2em; } .reveal p { text-align: left; } .reveal ol, .reveal dl, .reveal ul { display: block; } .reveal pre { border-radius: 3px; font-size: 0.7em; } .reveal code { padding: 2px 4px; border-radius: 3px; font-size: 90%; color: #c7254e; background: #2d2d2d; } </style> # Typescript workshop slide: https://hackmd.io/@msifeed/ts-workshop --- ## Оглавление - Система типов TypeScript - Зоопарк типов - Дженерики - Пересечения интерфейсов - Как выглядит ООП? - `interface` и `type`, в чем разница? - Сахар в TypeScript - `?? || ! ?` - Yarn 2 - Zero-install - Workspaces --- - Асинхронность в JS, варианты - Обработка ошибок, варианты - Реализация скраппера - Геттеры - Ивент-профайлы --- - `ts-mockito` - Что это за моки и где их использовать? - Как их использовать? - Что такое спаи??? - `typeorm` - Хитрости в Entities - Хитрости в QueryBuilder - `winston` - Как скопировать файл `logger.ts`? --- ## TypeScript - Зоопарк типов Примитивы: - `boolean` - `string` - неизменяемся строка - `number` - число с плавающей точкой, аналог `f64`, не твой бро - `bigint` - целочисленное число произвольной длины, твой бро. Константа пишется c приставкой `n` на конце: `123n`. --- ## TypeScript - Зоопарк типов Не-примитивы: - `[string, number, bigint]` - кортеж. Типы можно смешивать. - `bigint[]` - список. Также можно записывать как `Array<bigint>` - `(arg: string) => number` - функции - `new (arg: string) => Type` - функции-конструкторы - `{}` - пустой объект - `Record<string, number>` - объект (не `Map`) с типизированными значениями --- ## TypeScript - Зоопарк типов Прочие типы: - `void` - функция ничего не возвращает - `never` - функция никогда не возвращает - `undefined` - неинициализированная переменная или элемент какой-либо коллекции. "отстутствие чего-либо". - `null` - "наличие ничего" - `any` - наплевательский на тип тип, не твой бро - `unknown` - то же самое что `any`, но его нельзя использовать, только привести к другому типу - `object` - тип для не-примитивов. Сюда входят объекты, списки и функции. Не путать с глобальным `Object`! --- ## TypeScript - `interface` и `type` `type` используется для создания алиасов типов. ```ts type Address = string; type Vector3f = [number, number, number]; type Barbaz = { foobar: string; }; ``` `interface` описывает структуру объектов. Интерфейсы могут расширять другие типы объектов через `extends`, прямо как в джаве. ```ts type Foobar = { foo: string; bar(): void; }; interface IBaz extends Foobar, Beep, Boop { baz(): void; } ``` --- ## TypeScript - Анонимные типы Компилятор может принимать анонимные типы в качестве определенных типов если знает, что они точно подходят. Это применимо буквально ко всем переменным! ```ts interface Foobar { foo: string; bar: number; } function foobarize(fb: Foobar) { } const value = { foo: "something", bar: 123, extra: "field", }; foobarize(value); // OK! ``` --- ## TypeScript - Объединения и пересечения типов Объединения это оператор ИЛИ для типов. ```ts function foobar(arg: number | string) { } foobar("baz"); foobar(123); foobar(true); // нельзя! function getValueOrNot(): number | null { } ``` А пересечения это соответственно оператор И. ```ts function notify(ls: IMarketEventListener & IGeneralUserEventListener) { } class BothListener implements IMarketEventListener, IGeneralUserEventListener { /* ... */ } const ls = new Listener(); notify(ls); // OK! ``` --- ## TypeScript - Литеральный тип Используем строку-константу в качестве типа. Вместе с объединением типов получается своего рода строковый `enum`. ```ts function getBlock(num: number | "latest") { } getBlock(123); getBlock("latest"); type Operation = "lend" | "borrow" | "repay"; function foobar(op: Operation) { } foobar("lend"); foobar("borrow"); foobar("baz"); // нельзя! ``` --- ## TypeScript - `readonly` Не позволяет присваивать другое значение для поля класса или для элементов списка. ```ts function foobar(arg: readonly bigint[]) { } function foobar(arg: ReadonlyArray<bigint>) { } class Foobar { readonly foo: bigint; } ``` --- ## TypeScript - Функции-конструкторы Можно использовать в качестве метода-фабрики. ```ts class ProtocolScrapper implements IScrapperCore { marketScrapper: IMarketScrapper; constructor(ctor: new (c: IScrapperCore) => IMarketScrapper) { this.marketScrapper = new ctor(this); } } class Market implements IMarketScrapper { constructor(c: IScrapperCore) { } } const s = new ProtocolScrapper(Market); s.marketScrapper; // <- инстанс Market'а ``` --- ## TypeScript - Дженерики Дженерики повзоляют писать код, который может работать с разными типами. Их можно добавлять ко всем декларативным штукам: типам, функциям, интерфейсам и классам. ```ts type Handler<L> = (listener: L, l: Log, args: Result) => Promise<void>; function foobar<T>(arg: T): T { return arg; } foobar("baz"); foobar(123); const numbers: Record<number, string> = {}; const foobars = new Map<string, "foo" | "bar">(); numbers[123] = "boop"; foobars.set("pew", "foo"); foobars.set("paw", "bar"); ``` Очевидно в качестве дженериков можно использовать объединения и пересечения типов. --- ## TypeScript - Дженерики и наследования При наследовании от типа с дженериками необходимо указывать их типы. Это можно сделать явно прописав какой-то тип, либо добавить дженерики к дочернему классу и передать их типу-родителю. В этом примере мы ограничиваем ключи мапы `bigint`-ами, но тип значения все еще определяется дженериком. ```ts class IntMap<V> extends Map<bigint, V> {} const sm = new IntMap<string>(); sm.set(123n, "foo"); const nm = new IntMap<number>(); nm.set(456n, 123); ``` --- ## TypeScript - Дженерики и композиция Пример из скаппера. В нем дженерик из EventHandlerProfile определяет какого типа будет значение в коллбэке EventConsumer после парсинга аргументов Log'а. ```ts= type EventConsumer<L> = (listener: L, l: Log, args: any[]) => void; class LogHandler<L> { fragment: EventFragment; consumer: EventConsumer<L>; handle(listener: L, int: Interface, log: Log) { this.consumer(listener, log, int.decode(this.fragment, log)); } } class EventHandlerProfile<Listener> { int: Interface; handlers: Record<string, LogHandler<Listener>> = {}; handle(listener: Listener, log: Log) { this.handlers[log.topic].handle(listener, int, log); } } ``` --- ## `?? || ! ?` - `x?.y.z` - Опциональная последовательность. Делает все последующее выражение `undefined`, если значение перед вопросом равно `null` или `undefined`. - `x ?? y` - Оператор нулевого слияния. возвращает значение справа, если значение слева равно `null` или `undefined` ```ts interface IEntry { amount: bigint; } function fetch(): IEntry | null { /* ... */ } const value = fetch()?.amount ?? 0n; // ^-----^ IEntry | null // ^------^ IEntry | undefined // ^-------------^ bigint | undefined // ^-------------------^ bigint ``` Доки: [Optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining), [Nullish coalescing operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Nullish_coalescing_operator) - `x || y` - оператор ИЛИ. Выбирает левое значение если оно похоже на `true`. - `!` - злой брат-близнец `?`. Убирает `null` и `undefined` из объединенного типа. --- ## Yarn 2 - Workspace - Plug'n'Play - Zero-Installs --- ## Yarn Workspaces - Конфиг В корневом `package.json` перечисляются подмодули проекта. ```json { ... , "workspaces": [ "db-model", "indexer-api" ] } ``` Зависимости на пакеты из воркспейса указываются через `workspace:*` вместо номера версии. ```json { ... , "dependencies": { "protocol-indexer-api": "workspace:*", "protocol-indexer-db-model": "workspace:*" } } ``` --- ## Yarn Workspaces - Мелочи Из корня проекта можно вызывать команды всех дочерних проектов. В этом случае `pwd` будет папкой проекта. ```bash cd scrapper yarn test:scrapper ``` --- ## Yarn Plug'n'Play :+1: Генерирует `.pnp.cjs` который руководит всеми зависимостями проекта. Это все улучшает и ускоряет. Правда. :-1: Требуется поддержка IDE Некоторым плохим пакетам требуется указывать их зависимости вручную. Это делается в файле `.yarnrc.yml` в корне проекта. ```yaml packageExtensions: mocha@*: peerDependencies: dotenv: "*" ts-node: "*" ethers-multicall@*: peerDependencies: "@ethersproject/abi": "*" ``` --- ## Yarn Zero-Installs :+1: Уничтожен этап скачивания пакетов для запуска проекта :+1: Зависимости упакованы в архивы :-1: Требуется поддержка IDE :-1: Репозитории чуть дольше клонируются --- ## JS Async - Promise Промисы - это основа современной асинхронности в JS. Внутренняя функция промиса обещает вернуть значение или ошибку через внутренние колбеки. ```ts const p = new Promise<string>((resolve, reject) => { resolve("yay"); }) .then(msg => console.log(msg)); // Promise<void> ``` Похожий результат можно достигнуть с помощью async/await сахара. ```ts async function fn() { return "yay"; } const p = fn(); // Promise<string> console.log(await p); ``` Либо можно комбинировать подходы чтобы получить аналогичный результат. ```ts async function fn() { return "yay"; } const p = fn().then(msg => console.log(msg)); // Promise<void> ``` --- ## JS Async - Порядок выполнения Асинхронная таска переключается только если промис еще в ожидании получения значения. Довольно очевидно. ```ts async function immediate() { return "yay"; } async function slow() { return new Promise((res) => setTimeout(res, 1000)); } async function () { await immediate(); // Promise fulfilled - no context switch await slow(); // Promise pending - switch to other task await immediate(); // Promise fulfilled - no context switch }(); ``` --- ## JS Async - Исключения При использовании `await` внутри `async` функции исключения промисов можно отлавливать обычным try-catch блоком. ```ts async function foo() { /* ... */ } async function bar() { try { await foo(); } catch (e) { console.log("Got exception!", e); throw e; } } ``` А при вызове асинхронной функции из синхронного окружения настоятельно рекомендуется добавлять к промису колбек для ошибок. ```ts async function foo() { /* ... */ } foo().catch(e => { console.log("Got exception!", e); throw e; }); ``` --- ## JS Обработка ошибок - "`Option<T>`" Объединенный тип хорошо подходит в качестве опцинального возвращаемого значения. ```ts function foo(): string | null { return null; } const v = foo(); if (v) { /* внутри if блока v имеет тип string */ } else { /* а тут наоборот */ } ``` Для ценителей можно сделать так. ```ts type Option<T> = T | null; function foo(): Option<string> { } ``` --- ## JS Обработка ошибок - "`Result<T, E>`" Тут сложнее, потому что нет встроенного способа отличать "богатые" типы ошибок от не-ошибок. Есть библиотека `fp-ts` с кучей функциональных типов из Хаскеля, но они выглядят странно. Среди них есть аналог Result - [Either<E, A>](https://gcanti.github.io/fp-ts/modules/Either.ts.html). ```ts function foo(): Either<Error, string> { if (true) return Either.right("yay"); else return Either.left(new Error("meh")); } const val = foo(); if (val.isRight()) console.log("Ok"); ``` --- ## `ts-mockito` - Что это и зачем --- ## `ts-mockito` - Как это использовать? 1. Создаем мок-шаблон. ```ts let mockFoo: FooGetter; mockFoo = mock(); // Если тип известен, то его можно не писать const mockBar = mock<IBarGetter>(); ``` 2. Задаем значения которые он будет возвращать. В аругументах можно указать конкретные значения или `anything()`. ```ts when(mockFoo.getBlock(123, anything())).thenReturn({ block: 123, txs: [] }); when(mockFoo.getBlock(123, anything())).thenCall((a, b) => { block: 123, txs: [] }); ``` 3. Создаем инстанс мока, с которым будет работать остальной код. ```ts const foo = instance(mockFoo); // имеет тип FooGetter ``` 4. При вызове замоканного метода будет возращено то, что мы указали. Для остальных методов - `undefined`. ```ts foo.getBlock(123, "pew") // вернет указанную структуру ``` --- ## `ts-mockito` - Спаи Спаи позволяют изменять поведение реальных объектов как это делается с моками. ```ts const foo = new FooGetter(); const spiedFoo = spy(foo); when(spiedFoo.getBlock(123)).thenReturn({}); foo.getBlock(123); // {} ``` --- ```ts import "ts-mockito"; import { Mocker } from "ts-mockito/lib/Mock"; // Must be below import 'ts-mockito' // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types export function promiseMock<T>(cls?: (new (...args: any[]) => T) | (Function & { prototype: T })): T { const mocker = new Mocker(cls); mocker["excludedPropertyNames"] = ["hasOwnProperty", "then"]; return mocker.getMock() as T; } ``` ```ts const dbReaderMock = promiseMock<DBFacade>(); const spiedConnector = spy(dbConnector); // async getReader(): Promise<DBFacade> when(spiedConnector.getReader()).thenCall(() => instance(dbReaderMock)); ``` --- ## `typeorm` --- That's all folks!
{"metaMigratedAt":"2023-06-17T02:50:29.253Z","metaMigratedFrom":"YAML","title":"Typescript workshop","breaks":true,"slideOptions":"{\"transition\":\"fade\",\"display\":\"block\",\"history\":false}","contributors":"[{\"id\":\"0885321f-8771-4363-a8e5-07f9b68e84d0\",\"add\":19726,\"del\":8630}]"}
    447 views