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!