Skip to content

Instantly share code, notes, and snippets.

@serhii-lypko
Last active August 30, 2021 10:07
Show Gist options
  • Select an option

  • Save serhii-lypko/a9e71cadfebc2bf77cc74c3c6dd42472 to your computer and use it in GitHub Desktop.

Select an option

Save serhii-lypko/a9e71cadfebc2bf77cc74c3c6dd42472 to your computer and use it in GitHub Desktop.
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never;
type T10 = Foo<{ a: string, b: string }>; // string
type T11 = Foo<{ a: string, b: number }>; // string | number
type T22 = Foo<{ a: string }>; // never
/* * . * . * . * Type Systems vs Shapes * . * . * . */
/* . * . * . * . * . * . * . * . * . * . * . * . * . */
// Java и другие строго типизированные объектно ориентированные языки предполагают сравнение идентичности
// типов на основании того являются ли эти типы экзеплярами базового типа с которым происходит сравнение
// поскольку механника ООП в JS не является традиционной, TypeScript производит сравнение на основании
// соответсвия проверяемого типа shape'у базового типа, с которым происходит сравнение и если
// например некий объект содержит в себе все методы и функции, что и базовый тип, то он подходит
// дальше: сравнение происходит по принципу от wide (any ~ infinity) к narrow (конкретный тип, а дальше -> never)
// примерная диаграмма: any -> any[] -> string[] -> [string, string, string] -> ['abc', 'def', 'string'] -> never
// this is sometimes called “duck typing” or “structural subtyping”
// The idea behind structural typing is that two types are compatible if their members are compatible.
/* - - - - - * - - - - -
// TypeScript’s type system is erased, meaning that once your code is compiled,
// there is no persisted type information in the resulting JavaScript code.
- - - - - * - - - - - */
/* * . * . * . * . Interfaces . * . * . * . * . */
/* . * . * . * . * . * . * . * . * . * . * . * . */
Interface A {
a: number;
// опциональные типы полезны во-первых тем, что явно указывают тип для предполагаемого свойства, а во-вторых
// тем, что явно указывают имя, предотвращая опечатки и т.д
b?: boolean;
}
function makeFoo(obj: A) { ... }
const fooObj = {
a: 10,
b: false,
c: null
}
// вот здесь важно, что интерфейсы не предполагают "имплементацию" объектов (как в других языках),
// а только описывают их shape, а так же обязательные & опциональные свойства (порядок не имеет значения)
makeFoo(fooObj); // OK
// в то же время
makeFoo({ a: 10, b: 12, c: null }); // ERROR: ... object literal may only specify known properties, and 'c' ...
// для этого можно расширить интерфейс, таким образом, чтобы в него входили так же любые свойства, но
// при этом a и b обозначены явно
Interface A {
a: number;
b?: boolean;
[key: string]: any;
}
makeFoo({ a: 10, b: 12, c: null }); // OK
// - - - - - - - Function Types - - - - - - -
interface FunctionI {
(src: string, value: number): void;
}
// поскольку функция теперь знает все о своих аргументах, то можно не указывать их тип,
// а так же изменять именование параметров
const foo1: FunctionI = (src, val) => { ... }
// возможный вариант для function declaration
let foo2: FunctionI;
foo2 = function(src, val) { ... }
// - - - - - - - Indexable Types - - - - - - -
// здесь, создавая "словарь" с помощью связки [index: string]: number;
// компилятор в дальнейшем будет ожидать заданный тип от всех последующих свойст в объекте
interface NumberDictionary {
[index: string]: number;
length: number;
name: string; // error, the type of 'name' is not a subtype of the indexer
}
// хотя union ОК
interface NumberOrStringDictionary {
[index: string]: number | string;
}
// - - - - - - - Object creation & type assertion - - - - - - -
interface Shape {
color: string;
}
// нельзя создать пустой объект по заданному интерфейсу с обязательными полями
const figure: Shape = {}; // ERROR: Property 'color' is missing in type '{}' but required..
// или так же нельзя создать пустой объект, а затем попытаться записать туда поля
let emptyFigure = {};
emptyFigure.color = "red"; // ERROR: Property 'color' does not exist on type '{}'
// но возможно с использованием type assertion
let blackFigure = {} as Shape;
blackFigure.color = "black";
/* * . * . * . * . * . * Generics * . * . * . * . * . * . */
/* . * . * . * . * . * . * . * . * . * . * . * . * . * . */
/* - - - - - * - - - - -
// The main reason to use generics in TypeScript (and any other programming language) is to
// enable types (classes, types, or interfaces) to act as parameters.
// It helps us reuse the same code for different types of input since the type itself is available as a parameter.
// - Defining a relationship between input and output parameters types
// - Stronger type checks at compile time will be available
// - You can remove some unnecessary type casts
// важно, что в отличие от any, T позволяет сохранить и использовать в дальнейшем информацию о типе
// это крайне важно и позволяет опускать явную передачу типов при использовании дженерик функций и тд
- - - - - * - - - - - */
// generic interface для функции
interface FilterFunction<T = any> {
(val: T): boolean;
}
const stringFilter: FilterFunction<string> = (val) => typeof val === "string";
// - - - - - - - Type constrainsts - - - - - - -
// очень часто возникает необходимость в указании ограничений для какого-либо типа
// в данном случае с Permissions задача заключалась в том, чтобы функция принимала разные объекты,
// содержащие в себе permissions, а затем обновляла эти permissions в системе
// и generic, сохраняя тип (в отличие от any) с использованием constraints позволяет сделать все красиво 👌
type UserPermissions = {
canGetReports: boolean;
canSaveData: boolean;
}
interface LoginResponse extends UserPermissions {
userId: number;
token: string;
}
interface User extends UserPermissions {
id: number;
email: string;
}
// необходимо убедиться, что T будет содержать искомые Permissions
function savePermissions<T extends UserPermissions>(dataWithPermissions: T) {
const permissionsKeys = ["canGetReports", "canSaveData"];
const permissions = pick(dataWithPermissions, permissionsKeys);
// do stuff with permissions
}
savePermissions<LoginResponse>({
canGetReports: true,
canSaveData: false,
userId: 1,
token: "token_123"
});
// Type Parameters in Generic Constraints
// для того, чтобы убедиться, что K будет включать в себя один из ключей объекта obj
// инструкция keyof вернет список всех ключей, а extends запишет их как constraint
// (type K1 = keyof Person; // "name" | "age" | "location")
function getObjectValue<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const obj = {a: 1, b: 2};
const a1 = getObjectValue(obj, "a"); // Ok
const some = getObjectValue(obj, "some"); // ERROR: Argument.. is not assignable to parameter of type '"a" | "b"'.
// - - - - - - - Use cases - - - - - - -
interface Shape {
draw: () => void;
}
interface Circle extends Shape {
radius: number;
}
// излишняя абстракция
function drawShapesGeneric<T extends Shape>(shapes: T[]) {
shapes.forEach(shape => shape.draw());
}
// для того, чтобы выполнить отрисовку фигур, функции drawShapes
// вообще не нужно знать про остальные свойства этих фигур -> необходимости в generic нет
// (Правило: "you ask only what you need and return everything you can")
// поэтому для имплементации drawShapes достаточно использовать base class Shape
function drawShapes(shapes: Shape[]) {
shapes.forEach(shape => shape.draw());
}
const circle1: Circle = {
draw: () => {},
radius: 12
};
drawShapes([circle1]);
// пример правильного использования
interface Shape {
draw: () => void;
isDrawn: boolean;
}
interface Circle extends Shape {
radius: number;
}
// generic критически важен в ситуациях, когда важно сохранить тип на выходе функции
// здесь, просто передавая в drawShapesWithUpdate массив Circle на выходе этот массив Circle
// function drawShapesWithUpdate(shapes: S[]) -> return values asserts to Shape[]
// в итоге был бы преобразован в массив Shape (тип был бы упрощен)
// !!! generic здесь - "relationship between argument and return" !!!
// и именно generic (вместе с constraints) позволяет использовать
// правильный тип без его дальнейшей потери
function drawShapesWithUpdate<S extends Shape>(shapes: S[]): S[] {
return shapes.map(shape => {
shape.draw();
shape.isDrawn = true;
return shape;
});
}
const circles = [
{ draw() {}, isDrawn: false, radius: 10 },
{ draw() {}, isDrawn: false, radius: 12 },
];
const drawnShapes = drawShapesWithUpdate(circles);
function showRadiusForEachCircle(circles: Circle[]) {
circles.forEach(circle => {
console.log(circle.radius);
})
}
// иначе здесь была бы ошибка, поскольку функция ожидает именно Circle[], а не Shape[]
showRadiusForEachCircle(drawnShapes);
// * * *
type Dict<T> = {
[key: string]: T | undefined;
}
function mapDict<T, S>(dict: Dict<T>, fn: (val: T) => S ): Dict<S> {
const out: Dict<S> = {};
const keys = Object.keys(dict);
keys.forEach(key => {
const item = dict[key];
if (typeof item !== undefined) {
out[key] = fn(item);
}
});
return out;
}
// именно использование дженериков позволяет при использовании метода выполнять автоматический
// type infence из передаваемых аргументов
const res = mapDict(fileExtensions, e => `*.${e}`);
/* * . * . * . * . Types & Interfaces . * . * . * . * . */
/* . * . * . * . * . * . * . * . * . * . * . * . * . * . */
// одно из основных отличий между type alias и интерфейсами заключается в том, что alias могут типизировать
// примитивы и вообще любые сущности, то есть они более гибкие, в то время как интерфейсы могут описывать только
// объекты, сигнатуры функций и массивы
// выходит, что интерфейсы покрывают некоторое подмножество всех кейсов, которые покрываются alias, но
// в то же время предоставляют для этого более мощный и гибкий API
// self-referencing types dont work
type NumVal = 1 | 2 | 3 | NumArray[] // error
type NumArray = NumVal[]; // error
// interfaces cannot extends union types
type A = {
a: string;
} | { a: number };
interface A1 extends A {} // Error
/* * . * . * . Function signature overloading . * . * . * . */
/* . * . * . * . * . * . * . * . * . * . * . * . * . * . * . */
interface HasEmail {
name: string;
email: string;
}
interface HasPhone {
name: string;
phone: string;
}
type Method = "email" | "phone"
// возможные сигнатуры для функции
function sendMessage(method: "email", ...people: HasEmail[]): void;
function sendMessage(method: "phone", ...people: HasPhone[]): void;
function sendMessage(method: Method, ...people: (HasEmail | HasPhone)[]) {
people.forEach(send);
}
const peoples = [
{ name: "Dan", phone: "123" },
{ name: "Kan", phone: "321" },
]
sendMessage("phone", { name: "Dan", phone: "123" }); // OK
sendMessage("email", { name: "Dan", email: "abc@abc" }); // OK
sendMessage("email", { name: "Dan", phone: "123" }); // Error
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment