<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}]"}