Shorts
Krótkie informacje / sztuczki / rozwiązania, które mogą Ci się przydać w codziennej pracy 😉
Typowanie process.env
w TypeScript
Szybki, czysty i prosty sposób na typowanie process.env
w TypeScript z użyciem biblioteki zod.
import { z } from "zod";
const envVariables = z.object({
DATABASE_URL: z.string(),
CUSTOM_STUFF: z.string(),
});
envVariables.parse(process.env);
declare global {
namespace NodeJS {
interface ProcessEnv extends z.infer<typeof envVariables> {}
}
}
process.env.DATABASE_URL;
// ^ Auto-completion 🥳
Zobacz więcej tutaj.
Loose Autocomplete w TypeScript
Chcesz, aby zdefiniowany przez Ciebie typ union przyjmował również ogólnego string
-a, ale zachował możliwość automatycznego uzupełniania zdefiniowanych wartości? Możesz użyć pewnego tricku, aby to osiągnąć 🪄
Oto przykład:
// VS Code nie będzie podpowiadał wartości "foo", "bar" i "baz" 😢
type MyType = "foo" | "bar" | "baz" | string;
// Możemy podać dowolny string, ale VS Code będzie podpowiadał wartości "foo", "bar" i "baz" 🤩
type MyBetterType = "foo" | "bar" | "baz" | (string & {});
Teraz nasz typ jest bardziej elastyczny, ale nadal możemy korzystać z automatycznego uzupełniania w VS Code 💪
useCallback
i debounce
w React
Szybki snippet na to, aby połączyć ze sobą useCallback
oraz funkcję debounce()
(chyba nie muszę nikomu jej przedstawiać 😉) z biblioteki lodash
(albo z naszej własnej implementacji).
import { useMemo } from "react";
import debounce from "lodash.debounce";
function MyComponent() {
const changeHandler = () => {
// handle the event...
};
const eventHandler = () => {
// handle the event...
};
// Option A: useCallback() stores the debounced callback
const debouncedChangeHandler = useCallback(debounce(changeHandler, 300), []);
// Option B: useMemo() stores the debounced callback
const debouncedEventHandler = useMemo(() => debounce(eventHandler, 300), []);
// ...
}
Sprawdź dokładniejszy opis i więcej przykładów w tym artykule.
extends keyof
vs. in keyof
w TypeScript
Czy zdarzyło Ci się czasami trochę pogubić lub nie do końca rozumieć różnicę pomiędzy extends keyof
i in keyof
w TypeScript?
Oto krótkie wyjaśnienie:
extends keyof
jest używane w TypeScript dla typów generycznych. Wskazuje, że typ generyczny musi być kluczem określonego obiektu.
Oto prosty przykład:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(getProperty(x, "a")); // Output: 1
console.log(getProperty(x, "m")); // Error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.
W tym przykładzie K extends keyof T
oznacza, że K
powinien być kluczem obiektu T
. Dlatego TypeScript wyświetli błąd, jeśli spróbujesz użyć właściwości, która nie istnieje w obiekcie.
Wyrażenie in keyof
jest używane z typami mapowanymi, które są sposobem na tworzenie nowych typów na podstawie starych. Tak więc in keyof
pozwala nam iterować po kluczach typu obiektu.
Brzmi może nieco trudniej, ale spójrzmy na kolejny przykład:
interface User {
firstName: string;
lastName: string;
email: string;
age: number;
joinedDate: Date;
}
type AllStrings<T> = {
[K in keyof T]: string; // nasz nowy typ będzie zawierał wszystkie klucze z T
};
let user: User = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
age: 30,
joinedDate: new Date(2023, 1, 1),
};
let userInStrings: AllStrings<User> = {
firstName: user.firstName,
lastName: user.lastName,
email: user.email,
age: String(user.age),
joinedDate: user.joinedDate.toISOString(),
};
console.log(userInStrings);
// Output: { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', age: '30', joinedDate: '2023-02-01T00:00:00.000Z' }
Tutaj AllStrings<T>
jest typem mapowanym, który konwertuje wszystkie właściwości T
na ciągi znaków. Używamy in keyof
([K in keyof T]
), aby przejść przez każdy klucz w T
i przypisać mu nowy typ (w tym przypadku string
). W userInStrings
używamy tego zmapowanego typu do utworzenia obiektu, w którym wszystkie wartości są łańcuchami, co może być przydatne do niektórych rodzajów przetwarzania, serializacji, rejestrowania lub debugowania.
TypeScript Discriminated Unions
Bardzo często w naszych projektach możemy spotykać się z typem takim jak ten poniżej:
type Order = {
status: string; // "string" jest dość szerokim typem, a pewnie statusy zamówień są ograniczone do kilku wartości
name: string;
description?: string;
expectedDelivery?: Date; // czemu opcjonalny?
deliveredOn?: Date; // kiedy opcjonalny, a kiedy nie?
};
Ten sposób definiowania typów jest dość nieczytelny (czemu niektóre właściwości są opcjonalne?) i nieelastyczny. W takich przypadkach warto zastanowić się nad użyciem tzw. Discriminated Unions.
type Order = {
name: string;
description?: string;
} & (
| {
status: "ready"; // "expectedDelivery" i "deliveredOn" nie powinny tutaj się poajawić
}
| {
status: "inProgress";
expectedDelivery: Date; // Dostępne wtedy, gdy status jest "inProgress"
}
| {
status: "complete";
expectedDelivery: Date; // Dostępne wtedy, gdy status jest "complete"
deliveredOn: Date; // Dostępne wtedy, gdy status jest "complete"
}
);
Ten zapis może wydawać się bardziej rozwlekły, ale działa już jako prosta dokumentacja domeny, usuwa mnóstwo niejasności i pozwala pisać bardziej przejrzysty kod. Teraz TS będzie w stanie "domyśleć" się, które właściwości są wymagane w zależności od statusu zamówienia.
Rozszerzenie tego tematu można znaleźć w ciekawych artykule: How I ease the next developer reading my code
infer
- zaawansowany typ warunkowy w TS
W TypeScript możemy wykorzystać typy warunkowe wraz z słowem kluczowym infer
do wnioskowania o typach. Przyjrzyjmy się poniższemu przykładowi:
type ArrayTypes<T> = T extends (infer U)[] ? U : never;
let arr = [1, "2", []];
type test = ArrayTypes<typeof arr>; // typ test = string | number | any[]
W tym przykładzie, typ ArrayTypes<T>
bierze typ T
i sprawdza, czy T
jest typem tablicy. Jeśli tak, to inferuje typ U
z tablicy T[]
i zwraca U
. W przeciwnym razie zwraca never
.
Ważne jest, aby zauważyć, że infer
musi być podłączony do U
, a nie do []
. Dlatego korzystamy z nawiasów w (infer U)[]
. Bez nawiasów, TypeScript zinterpretowałby to jako infer (U[])
, co byłoby niepoprawne, ponieważ infer
może być używane tylko do inferencji typów, a nie struktur typów takich jak tablice【33†source】.
Kolejny przykład, tym razem z obiektem. W tym przypadku infer
stworzy nam typ, który będzie zawierał wszystkie typy wartości obiektu:
type ObjectTypes<T> = T extends { a: infer U; b: infer U } ? U : never;
let obj = { a: 1, b: "2" };
type Test = ObjectTypes<typeof obj>; // typ Test = string | number
Jak widać możliwości tutaj są ogromne, więc warto zapoznać się z tym tematem.
Typy warunkowe w TypeScript
Typy warunkowe w TypeScript pozwalają na wybór typu w zależności od warunku. Jest to dość przydatne narzędzie, które pozwala na tworzenie bardziej złożonych typów.
type IsString<T> = T extends string ? true : false;
type X = IsString<"hello">; // true
type Y = IsString<number>; // false
type Z = IsString<{ name: string }>; // false
W tym prostym przykładzie, IsString
jest typem warunkowym, który sprawdza, czy podany typ T
jest stringiem. Jeśli tak, to zwraca true
, a w przeciwnym razie zwraca false
.
Pamiętaj, że typy warunkowe mogą być zagnieżdżone i mogą używać dowolnej logiki (możemy np. używać tego sprawdzenia w if
-ach i zawężać bardziej typowanie), która jest dostępna w typach, co czyni je niezwykle elastycznymi i potężnymi narzędziami do modelowania typów w TypeScript.
unknown
vs any
w TS
W TypeScript typ unknown
jest bezpieczniejszym odpowiednikiem typu any
. Podczas gdy any
pozwala nam na wszystko, unknown
wymusza sprawdzanie typu przed wykonaniem operacji.
let foo: any = "hello";
foo = foo.split("").reverse().join(""); // Zadziała, ale...
let foo2: any = { greet: "hello" };
foo2 = foo.split("").reverse().join(""); // TS nie wykryje błędu, ale dostaniemy błąd w konsoli
let bar: unknown = "world";
bar = bar.split("").reverse().join(""); // TS podkreśli nam błąd w edytorze, ponieważ nie możemy użyć metody "split" na nieznanym typie.
bar = (bar as string).split("").reverse().join(""); // OK, ponieważ zadeklarowaliśmy typ, ale może być lepiej...
bar = typeof bar === "string" ? bar.split("").reverse().join("") : bar; // Jeszcze lepiej, ponieważ sprawdziliśmy typ
Z unknown
TypeScript zapewnia, że musiz sprawdzić typ przed wykonaniem operacji, więc jest to dużo bezpieczniejsze podejście niż any
.
Aliasy podczas importów w TS
Macie już dość kropkowania podczas importów? Aliasy w TS mogą nam tutaj znacznie uprościć życie. Do pliku tsconfig.json
wystarczy dodać następującą konfigurację:
{
"compilerOptions": {
// Your other options...
"paths": {
"@/*": ["./src/*"]
}
}
}
Od teraz możemy zaczynać nasze importy od zdefiniowanego wcześniej poziomu w naszej aplikacji:
import { Button } from "@/components/Button"; // 🤩 import z "src/components/Button"
import { OtherButton } from "../../../components/Button"; // 😥
Taka składnia jest dużo czytelniejsza i łatwiejsza do utrzymania. Jeśli zmienimy zagnieżdżenie bądź położenie folderu, to nie musimy martwić się o zmianę ścieżek w naszym kodzie 💪.
Wartość domyślna dla każdego klucza w obiekcie
Czasami chcemy (niezbyt często, ale może się zdarzyć 😉), aby właściwości obiektu były automatycznie inicjalizowane. Powszechnym sposobem radzenia sobie z tym jest korzystanie z metod getter
i setter
. Jednak może to stać się uciążliwe, jeśli mamy wiele właściwości. Właśnie tutaj obiekt Proxy może okazać się bardzo pomocny.
Proxy
w JavaScript jest używane do definiowania niestandardowego zachowania dla podstawowych operacji (np. wyszukiwanie, przypisanie, wyliczanie, wywoływanie funkcji itp.) obiektów.
Proxy możemy również użyć więc również do automatycznego inicjowania właściwości obiektów, których właściwości nie są do końca znane:
const autoInit = (defaultValue) =>
new Proxy(
{},
{
get: (target, name) =>
name in target ? target[name] : (target[name] = defaultValue),
}
);
const myObject = autoInit([]);
console.log(myObject.foo); // []
myObject.foo.push("bar");
console.log(myObject.foo); // ['bar']
W tym przykładzie każda właściwość myObject
, która jest dostępna przed jej ustawieniem, zostanie automatycznie zainicjowana na pustą tablicę (lub dowolną inną domyślną wartość, którą podamy). Może to być dość przydatne w sytuacjach w których chcemy uniknąć wartości undefined
.
Szybkie sprawdzanie właściwości obiektu w JavaScript
Czy wiedziałeś, że możesz sprawdzić, czy obiekt ma daną właściwość używając operatora in
? Oto jak to zrobić:
const myObject = { foo: "bar" };
console.log("foo" in myObject); // true
console.log("baz" in myObject); // false
Szybko i czysto, bez potrzeby sprawdzania undefined
😉
Readonly w TS
W języku TypeScript można użyć modyfikatora readonly
, aby wskazać, że właściwość klasy lub interfejsu jest tylko do odczytu, co oznacza, że po przypisaniu do niej wartości nie można jej zmienić. Oto krótki przykład:
interface Point {
readonly x: number;
readonly y: number;
}
let p: Point = { x: 10, y: 20 };
p.x = 5; // Error: Cannot assign to 'x' because it is a read-only property
W powyższym kodzie x
i y
w Point
są właściwościami tylko do odczytu 💪.
Brak mocków w testach z React Router (v6)
i React Testing Library
Customowy render()
, za pomocą którego możemy łatwo testować komponenty wykorzystujące nawigację w naszej aplikacji:
import React, { isValidElement } from "react";
import { render } from "@testing-library/react";
import { RouterProvider, createMemoryRouter } from "react-router-dom";
export function renderWithRouter(children, routes = []) {
const options = isValidElement(children)
? { element: children, path: "/" }
: children;
const router = createMemoryRouter([{ ...options }, ...routes], {
initialEntries: [options.path],
initialIndex: 1,
});
return render(<RouterProvider router={router} />);
}
Dokładniejsze omówienie powyższego kodu oraz przykładowe użycie do znalezienia w tym artykule.
TS type guards
dla tablic danych
Mając tablicę danych, która może zawierać różne typy (czyli np. nie jesteśmy pewni odpowiedzi z API), to możemy użyć type guard i metodę .every()
zamiast if-ów
, żeby zawęzić sobie typowanie.
// type guard z użyciem słowa kluczowego "is"
function isArrayOfNumbers(arr: unknown[]): arr is number[] {
return arr.every((element) => typeof element === "number");
}
const arr = [1, 2, 3, 4, 5];
if (isArrayOfNumbers(arr)) {
console.log(arr[0].toFixed); // ✅ OK, wiemy, że "arr" jest tablicą liczb
} else {
console.log(arr[0].toFixed); // ⛔ ERROR, nie możemy użyć metody ".toFixed()" na typie "unknown"
}
Powyżej mamy prosty przykład z tablicą liczb, ale możemy też użyć tego rozwiązania do bardziej skomplikowanych typów danych.