Dalej poznajemy nudne początki TSa. Konieczne, aby pójść do przodu z materiałem. Kontynuacja lekcji poprzednich.
Ok, interfejs generyczny:
interface GenericAdd<AddType> {
add: (x: AddType, y: AddType) => AddType;
}
class GenericNumber implements GenericAdd<number> {
add(x: number, y: number) {
return x + y;
} // number + number
}
class GenericString implements GenericAdd<string> {
add(x: string, y: string) {
return x + y;
} // string + string
}
const genericNumber = new GenericNumber();
genericNumber.add(1, 2); // 3
const genericString = new GenericString();
genericString.add("Hello", ", Mammals!"); // Hello, Mammals!
Raczej widzimy o co chodzi. Teraz typ Required:
interface Props {
a?: number;
b?: string;
}
const obj: Props = { a: 5 };
const obj2: Required<Props> = { a: 5 };
I tutaj obj2 ma obowiązek mieć wszystkie pola (czyli i a i b) wypełnione. Mamy też partial, czyli przeciwieństwo required. I poza takim najbardziej podręcznikowym przykładem konstrukcji obiektu krok po kroku, lepszy przykład to jest update obiektu:
interface Todo {
title: string;
description?: string;
}
const todo1 = {
title: "organize desk",
extra: "metadata", // duck typing is allowed!
};
const updateTodo = (
todo: Todo,
fieldsToUpdate: Partial<Todo> // allow partial updates
) => ({ ...todo, ...fieldsToUpdate });
const result1 = updateTodo(todo1, {
description: "throw out trash",
});
Czyli tak, ma być partial Todosa przekazany. Czyli albo title, albo description, w zależności od tego, czemu chcemy update zrobić. Nie musimy przekazywać od razu wszystkich wartości, jedną możemy, ale z obiektu todo.
Możemy też połączyć jedno z drugim:
const todo2 = {
...todo1,
description: "clean up", // call bombs without description
};
const updateRequiredTodo = (
todo: Required<Todo>,
fieldsToUpdate: Partial<Todo>
): Required<Todo> => ({ ...todo, ...fieldsToUpdate });
const result2 = updateRequiredTodo(todo2, {
description: "throw out trash",
});
Ok, chciałbym, abyśmy jeszcze raz przypomnieli sobie ten przykład:
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
My tutaj jako kind przypisujemy konkretną wartość, zwróćmy na to uwagę. A to sprawia, że jeżeli sprawdzimy, że obiekt ma kind ustawiony na taką wartość, to TS domyśli się, jakie properties ma a jakie nie ma.
To jest częsty wzorzec pracowania z takimi rzeczami:
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
}
}
TS po prostu wie, jak sprawdzimy ifem albo switchem kind, jakie properties ma shape. Ok, poznajmy jeden z nielicznych wyjątków, gdzie przydaje się typ never:
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape) {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
//Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
To wynika z faktu, że żadnego typu do never przypisać się nie da. A my tutaj mamy przyjmowany typ shape, którym może być triangle, ale nigdzie tego Triangle nie wyłapujemy.
Ten sposób sprawia, że nigdy się na coś takiego nie nadziejemy. TS nam mówi – przyjmujesz shape, ale nie zabezpieczyłeś wszystkich możliwości, odnośnie tego czym shape może być.
To wszystko są bardzo przydatne wzorce. Więcej TSa niedługo!