Programista - pytania rekrutacyjne - TypeScript - okładka

Programista – pytania rekrutacyjne – TypeScript

Opublikowano Kategorie Frontend, Praca w IT, TypeScriptCzas czytania 13min

Jedną z części rozmowy rekrutacyjnej jest rozmowa techniczna. Często podczas tej części rozmowy rekruter poprosi Cię o opisanie projektów, w których do tej pory brałeś(aś) udział. Warto wtedy opisać czego się nauczyłeś(aś), jakie trudności napotkałeś(aś) oraz jak udało Ci się z nimi uporać. W wielu przypadkach rekruterowi to wystarczy. Gdy jednak nie masz zbyt wiele doświadczenia, to rekruter będzie chciał sprawdzić Twoją wiedzę poprzez serię kilku pytań technicznych.

Część pytań pochodzi z mojego e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer, który możesz odebrać, zapisując się na mój mailing. Nie wszystkie pytania zawarte w tym artykule znajdziesz w moim e-booku, więc zachęcam Cię do przeczytania tego wpisu, nawet jeśli planujesz przeczytać e-booka.

Na blogu tematyce pytań technicznych poświęciłem szerszą uwagę w serii wpisów. Zachęcam do sprawdzenia pozostałych artykułów:

Ten zestaw pytań dedykowany jest coraz popularniejszemu nadzbiorowi języka JavaScript, czyli TypeScriptowi. Pytania z tej serii możesz również traktować jako trening i sprawdzenie swoich umiejętności przed rozmową rekrutacyjną.

Czym się różni typ any od typu unknown?

Typ any, jak sama nazwa sugeruje, mówi, że zmienna tego typu może być czymkolwiek. Oznacza to, że gdy w danym miejscu kompilator spodziewa się przekazania konkretnego typu, to można tam również przekazać wartość typu any i nie spowoduje to błędu transpilacji. Na przykład:


const anyValue: any = true;

function x( a: string ): void {
   // code
}

function y( a: number ): void {
   // code
}

x( anyValue );
y( anyValue );

W obu przypadkach kompilator TypeScript nie zasygnalizuje błędu. W tym miejscu należy powiedzieć, że korzystanie z typu any powinno się zredukować do minimum. Jedną z głównych zalet TypeScriptu jest właśnie typowanie, więc korzystanie z any, gdy nie jest to absolutnie konieczne, jest według mnie jednoznacznie złą praktyką. Zalecam ustawienie w pliku tsconfig.json wartości noImplicitAny na true oraz zastosowanie reguły @typescript-eslint/no-explicit-any w konfiguracji ESLint.

Typ unknown na pierwszy rzut oka jest bardzo podobnym typem. Tutaj również warto zwrócić uwagę na nazwę. Tak jak nazwa any sugeruje, że wartość może być czymkolwiek, tak typ unknown sugeruje, że typ wartości pozostaje nieznany. Typ unknown nie pozwala na pewne rzeczy, na które pozwala typ any, jest nieco bardziej restrykcyjny. Po pierwsze, do zmiennej określonego typu, np. number można przypisać wartość typu any, lecz nie można przypisać wartości typu unknown. Po drugie, na wartościach typu unknown nie możemy wywoływać metod zarezerwowanych dla innych typów np. toString czy isNan. Oczywiście dotyczy to też bardziej złożonych czy typów zdefiniowanych przez nas samych. Tu również przydatny okaże się przykład:


const a: any = true;
const b: unknown = true;

let c: number;

c = a; // Ok
c = b; // Błąd - Object is of type 'unknown'.(2571)

a.toString(); // Ok
b.toString(); // Błąd - Object is of type 'unknown'.(2571)

Oczywiście istnieje możliwość wywołania metody toString na wartości typu unknown. Można to zrobić przez skorzystanie z operatora as i casting na inny typ zawierający tę metodę, ale to również nie należy do dobrych praktyk. Z obu typów, zdecydowanie lepiej jest wybrać typ unknown, ale jeszcze lepiej jest redukować użycie obu typów do niezbędnego minimum, co oczywiście nie zawsze jest możliwe.

Czy da się w jakiś sposób „wyłączyć/zawiesić kompilator TypeScript”, na przykład dla konkretnej linii kodu? Jeśli tak, to w jaki sposób?

Odpowiedź brzmi — tak, da się. Pytanie celowo nie zawiera słowa „zignorować”, mimo że ono tam idealnie pasuje, ponieważ to ono jest kluczem do odpowiedzi na powyższe pytanie. TypeScript oferuje kilka klauzul pozwalających na dodawanie wyjątków dla kompilatora:

  • @ts-ignore – w przypadku gdy kompilator TypeScript zasygnalizuje wystąpienie błędu, umieszczenie tej klauzuli linię wyżej, pozwala na zignorowanie tego błędu przez kompilator.
  • @ts-expect-error – klauzula umieszczana jest w miejscu, gdzie programista spodziewa się wystąpienia błędu. Różnica między tą klauzulą a @ts-ignore jest taka, że jeżeli błąd nie wystąpi, to samo wystąpienie klauzuli w kodzie będzie interpretowane jako błąd.
  • @ts-nocheck – cały kod poniżej tej klauzuli nie będzie sprawdzany przez kompilator TypeScript. Jest to blokowy odpowiednik dla @ts-ignore.
  • @ts-check – odwrotność powyższej klauzuli pozwalający na przywrócenie pełnego sprawdzania kodu.

Stosowanie tych klauzul powinno być ostatecznością. Zdecydowanie lepiej jest znaleźć przyczynę problemu i naprawić problem niż korzystać z tych klauzul. Uzasadnionym przypadkiem użycia może być wykorzystanie błędnie otypowanych bibliotek. Oczywiście zawsze można spróbować wystawić pull requesta do takiej biblioteki, ale nie każda biblioteka jest aktywnie utrzymywana i nie zawsze jest na to czas.

Warto także rozważyć włączenie reguły @typescript-eslint/ban-ts-comment w swojej konfiguracji ESLint z wartością allow-with-description oraz ustawionym parametrem minimumDescriptionLength. Sprawi to, że ESLint zaakceptuje powyższe klauzule, ale tylko jeżeli będą zawierały wyjaśnienie o pewnej wymaganej długości znaków. Dzięki temu, każde użycie klauzul będzie nieco bardziej przemyślane i wyjaśnione szerzej w kodzie niż tylko „bug” czy „won’tfix”.

Typy generyczne w TypeScript - kotek

Czym są tzw. „generyki”? Podaj przykład zastosowania.

„Generyki” lub inaczej typy generyczne pozwalają na dynamiczne przekazanie typu do konstrukcji w kodzie, na przykład funkcji czy interfejsów. Myślę, że najlepiej będzie to pokazać na przykładzie. Załóżmy, że mamy API, które zwraca listę kotków oraz piesków. Ponadto lista ta wspiera paginację. Czyli zwrócony obiekt z API będzie zawierał: numer strony, kursor do poprzedniej strony, kursor do następnej strony, oraz listę zwróconych kotków lub piesków. Poniższy przykład pokazuje, jak można stworzyć reużywalny interfejs, który będzie w stanie obsłużyć zarówno kotki, jak i pieski:


interface ICat {
   // Properties
}

interface IDog {
   // Properties
}

interface IPaginationResult<T> {
   page: number;
   prev_page: null | string;
   next_page: null | string;
   items: T[];
}

function getDogs(): IPaginationResult<IDog> {
   // code
}

function getCats(): IPaginationResult<ICat> {
   // code
}

Dzięki użyciu generyków, za pomocą jednego interfejsu zdefiniowany został kształt zwracanego obiektu dla dwóch metod. Co więcej, nic nie stoi na przeszkodzie, aby użyć tego interfejsu do zwrócenia np. ptaszków czy rybek w przyszłości.

Co oferuje TypeScript w zakresie definiowania właściwości klasy w konstruktorze? Czy można w jakiś sposób skrócić zapis?

TypeScript oferuje możliwość skróconego definiowania właściwości przekazanych przez konstruktor. Dzięki temu taki kod


class Foo {
   private readonly _a: string;
   private readonly _b: string;
   private readonly _c: string;

  constructor( a: string, b: string, c: string ) {
    this._a = a;
    this._b = b;
    this._c = c;
  }
}

można zastąpić następującą formą.


class Foo {
  constructor(
     private readonly _a: string,
     private readonly _b: string,
     private readonly _c: string
   ) {}
}

Uważam taki zapis za znacznie bardziej czytelny i miły dla oka.

Czym są typy enum oraz tuple?

Typ enum, czyli inaczej typ wyliczeniowy pozwala na zdefiniowanie zestawu nazwanych stałych, zarówno w formie numerycznej, jak i formie łańcucha znaków.


enum Colors {
  Red,
  Green,
  Blue
}

console.log( Colors.Red ); // 0

Tak zdefiniowany typ wyliczeniowy przypisze rosnąco liczby całkowite, zaczynając od 0, dla każdego zdefiniowanego elementu. Oczywiście można również zdefiniować te liczby ręcznie.


enum Colors {
  Red = 1,
  Green = 2,
  Blue = 3
}

console.log( Colors.Red ); // 1

Możliwe również jest zdefinowanie wartości jako łańcucha znaków.


enum Colors {
  Red = '#FF0000',
  Green = '#00FF00',
  Blue = '#0000FF'
}

console.log( Colors.Red ); // '#FF0000'

Z kolei typ tuple lub też inaczej krotka, jest strukturą danych pozwalającą na zdefiniowanie listy z wartościami o konkretnych typach w konkretnych polach. Można doszukać się tu pewnych analogii do struktur tabel i rekordów w relacyjnych bazach danych.


let user: [ string, number, string ]; // Imię, wiek, zawód

user = [ 'Steve', 21, 'Programmer' ]; 

Typy użytkowe w TypeScript - pędzle

Wymień kilka typów użytkowych (utility types).

Pierwszy z typów to typ Partial<T>. Znajduje on zastosowanie w momencie, gdy mamy interfejs gdzie wszystkie z pól mogą być opcjonalne. Zamiast definiować każde z pól interfejsu jako opcjonalne, można wykorzystać do tego celu typ Partial<T>.


interface IExample {
   foo: string;
   bar: string;
   baz: number;
}

const a: Partial<IExample> = {
   foo: 'foo'
};

Powyższy kod jest w pełni poprawnym kodem dla kompilatora TypeScript, mimo że obiekt a nie zawiera wszystkich włściwości interfejsu. Może to zostać wykorzystane na przykład, gdy kod oczekuje, przekazania jakiegoś obiektu, np. fragmentu konfiguracji, która zawiera predefiniowane wartości domyślne.

Kolejny użyteczny typ to Require<T>, który jest przeciwieństwem typu Partial<T>. W tym przypadku wszystkie właściwości przekazanego interfejsu są wymagane, niezależnie od tego, czy zostały oznaczone jako opcjonalne, czy też nie.


interface IExample {
   foo?: string;
   bar: string;
   baz: number;
}

const a: Required<IExample> = {
   bar: 'bar',
   baz: 3
}; // Property 'foo' is missing in type '{ bar: string; baz: number; }' but required in type 'Required<IExample>'.(2741)

Inny przydatnym typem jest typ Readonly<T>. Sprawia on, że wszystkie właściwości stają się właściwościami tylko do odczytu.


interface IReadonlyExample {
   readonly foo: string;
   readonly bar: string;
}

interface IExample {
   foo: string;
   bar: string;
}

// Readonly<IExample> === IReadonlyExample

Następny typ warty uwagi to typ Record<K,V> pozwalający na zdefiniowanie mapy klucz-wartość. Jest on znacznie bardziej użyteczny niż typ object, gdyż pozwala na zdefiniowanie ścisłego zestawu lub typu kluczy oraz wartości. Dzięki temu, typowanie za pomocą Record<K,V> jest o wiele bardziej precyzyjne, niż typowanie za pomocą typu object.

Samych typów użytkowych jest sporo więcej, a całą listę znajdziesz w dokumentacji TypeScript. Te, które przedstawiłem, są jednymi z najczęściej przeze mnie wykorzystywanych.

Jakie typowanie wykorzystuje TypeScript i jakie niesie to konsekwencje?

TypeScript wykorzystuje typowanie strukturalne, co niesie za sobą konsekwencje zarówno pozytywne, jak i negatywne.

Pierwszą konsekwencją jest to, że pozwala to na pewną dowolność w przypisywaniu wartości do zmiennych o określonym typie. Kompilator TypeScript sprawdza jedynie, czy struktura przypisywanego obiektu jest zgodna ze strukturą oczekiwaną.


interface Pet {
   name: string;
}

interface Person {
   name: string;
}

let steve: Person;

const nemo: Pet = {
   name: 'nemo'
}

steve = nemo;

Jest to broń obosieczna, z uwagi na to, że programista zyskuje większą elastyczność, lecz kosztem sytuacji gdzie możemy przypisać obiekt tylko na podstawie zgodności interfejsów. W przykładzie powyżej osoba steve została zwierzakiem o imieniu nemo.

Kolejną konsekwencją typowania strukturalnego jest to, że w przypadku typowania przy użyciu klas sprawdzane są wszystkie pola klasy, również te prywatne. Jest to w pewien sposób zrozumiałe i oczekiwane zachowanie z punktu widzenia typowania strukturalnego, ale w większych projektach, gdzie wykorzystane są moduły w różnych wersjach z subtelnymi zmianami w klasach, może to powodować sporo problemów. Co więcej, prywatne właściwości nie są dostępne spoza klasy, a mimo to wciąż mają swój udział w typowaniu co również może powodować spore problemy. Stąd też zdecydowanie odradzam typowanie przy użyciu klas na rzecz interfejsów. Zachęcam też do przeanalizowania poniższego przykładu.


class Person {
   private _age: number;

   constructor( private readonly _name: string ) {
      this._age = 21;
   }
}

class Pet {
   constructor( private readonly _name: string ) {}
}

let steve: Person;

const nemo: Pet = new Pet( 'nemo' );

steve = nemo; // Błąd - Property '_age' is missing in type 'Pet' but required in type 'Person'

Czym są type guards i w jakim celu się je stosuje?

Type guards to konstrukcja wykorzystywana do doprecyzowania typu w przypadku gdy zmienna może przyjmować więcej niż jeden typ.


const a: string | number = getValue();

function isNumber( a: string | number ): a is number {
  return typeof a === "number";
}

W tym przypadku getValue może zwrócić zarówno typ string, jak i number. Użycie type guard pozwala zweryfikować jaki typ został użyty. Oczywiście można by tutaj pokusić się o skorzystanie tylko z operatora typeof, natomiast użycie type guard powoduje zapisanie informacji o typie zmiennej, przez co nie jest konieczne każdorazowe sprawdzanie jej typu.

Wymień główne korzyści jakie niesie ze sobą TypeScript.

Myślę, że to pytanie powinno paść jako jedno z pierwszych przy dyskusji o TypeScript, ponieważ rozbudowana odpowiedź na nie może zawierać odpowiedź również na inne pytania z tego wpisu.

Największą zaletą płynącą z TypeScriptu są oczywiście typy! Przydatne są zarówno typy wbudowane, jak i możliwość tworzenia własnych typów i interfejsów.

TypeScript oferuje naprawdę sporo możliwości w zakresie typowania, co pozwala tworzyć o wiele czystszy kod niż w JavaScript. Typy pozwalają wyeliminować lub zredukować istotny odsetek błędów, które można popełnić w JavaScript, a na które nie pozwoli kompilator TypeScript. Kod w TS jest znacznie bardziej przewidywalny i łatwiejszy w debugowaniu.

Kolejna z zalet to domyślna implementacja eksperymentalnych funkcji tj. dekoratory czy optional chaining. Oprócz tego TypeScript oferuje modyfikatory dostępu, które w czystym JavaScript są od niedawna.

TypeScript pozwala też na wykorzystywanie kodu JavaScript, co oznacza, że nie jest konieczne przepisywanie całego codebase od razu. Można to robić stopniowo.

Podsumowanie

Te kilkanaście pytań to o wiele za mało, by być dobrze przygotowanym do rozmowy rekrutacyjnej. Jeszcze raz gorąco zachęcam do sprawdzenia innych wpisów na blogu oraz pobranie e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer. Zachęcam też do zostawiania komentarzy i udostępnienia tego wpisu znajomym, którym ta wiedza może się przydać.

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Zapisując się na mój mailing, otrzymasz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Powiadom o
guest

2 komentarzy
oceniany
najnowszy najstarszy
Inline Feedbacks
View all comments
Impress
Impress
1 rok temu

const a: any = true;

function x( a: string ): void {
// code
}

function y( a: number ): void {
// code
}

x( a );
y( a );

Ten przykład jest błędny. Dlaczego kompilator potencjalnie miałby wyrzucić błąd? Nawet jeśli zmienna stała „a” nie byłaby typu any to błędu i tak by nie było. Przecież „a” w funkcjach to są parametry i nie są w żaden sposób związane ze zmienną.