Ciekawostki Typescript #2 - TS - typy = JS

2020-10-01

TypeScript nadzbiorem JavaScriptu

Mówi się, że “TypeScript jest nadzbiorem JavaScriptu”. Ale co to oznacza? W praktyce - wiadomo - każdy kod JavaScriptu jest poprawnym kodem JavaScriptowym. Zatem, co jest tym “nad” w stosunku do JavaScriptu? Otóż - system typów. I - przynajmniej koncepcyjnie, z pewnymi drobnym zastrzeżeniami, o których niżej - tylko system typów. To znaczy, że - “kanonicznie” (zastrzeżnia niżej) - konwersja TS do JS polega tylko na usunięciu informacji o typach. A zatem właściwie taką “konwersję” można by wykonać ręcznie usuwając wszystkie typy czy informacje, że dana klasa implementuje jakiś interfejs.

To, co napisałem powyżej, można dobrze zaobserwować na przykładzie typescriptowych modyfikatorów dostępu w klasie. Wyobraźmy sobie, że mamy taką klasę, napisaną w TS:

class Person {
  private firstName: string
  private lastName: string

  constructor(firstName: string, lastName: string) {
    this.firstName = firstName
    this.lastName = lastName
  }

  public sayHello(): string {
    return `Hi, I'm ${this.firstName} ${this.lastName}!`
  }
}

Pola firstName i lastName zadeklarowaliśmy jako prywatne. Zatem w TS próby dostania się do nich zakończą się błędem:

const john = new Person("John", "Smith")
console.log(john.firstName) // <- tu będzie błąd

Jak ta klasa zostanie jednak przekonwertowana do JavaScriptu? Mniej więcej tak:

class Person {
  constructor(firstName, lastName) {
    this.firstName = firstName
    this.lastName = lastName
  }

  sayHello() {
    return `Hi, I'm ${this.firstName} ${this.lastName}!`
  }
}

A gdybyśmy w JavaScripcie chcieli się dostać do firstName? Proszę bardzo - błędu nie ma:

const john = new Person("John", "Smith")
console.log(john.firstName) // <- błędu nie ma!

Gdy pierwszy raz się o tym dowiedziałem, to pomyślałem: “Ale jak to? Prywatne pole jest publiczne?… TypeScript, oszukałeś mnie…” Ale w zasadzie to jest konsekwencja tego równania:

TypeScript - (system typów) = JavaScript

Jeśli zatem chcielibyśmy na 100% traktować te pola jako prywatne - bo np. piszemy bibliotekę i naprawdę chcemy ukryć jakiś szczegół implementacyjny - to mamy dwa wyjścia:

  • skorzystanie z eksperymentalnej notacji pól prywatnych w JS - # (tutaj opis tej notacji),
  • skorzystanie z domknięć (patrz niżej).

W przypadku domknięć, należałoby napisać w TS naszą klasę mniej więcej tak:

class Person {
  public sayHello: () => string

  constructor(firstName: string, lastName: string) {
    this.sayHello() = () => {
      return `Hi, I'm ${firstName} ${lastName}!`
    }
  }
}

To rozwiązanie też ma niestety swoje wady:

  • jeśli chcemy skorzystać z ukrtych danych w metodzie klasy, to musimy ją zdefiniować w konstruktorze,
  • każda instacja klasy ma kopię tych metod zdefiniowanych w konstruktorze, co zwiększa zużycie pamięci.

Wyjątki od reguły

Na koniec muszę zwrócić uwagę na dwa wyjątki od tej zasady, że transpilator jedynie usuwa informacje o typach i modyfikatory dostępu. Tymi wyjątkami są enumy oraz pewien syntactic sugar w postaci definiowania parametrów konstruktora od razu jako pól klasy.

Enumy

Dla programisty C#, który zaczyna przygodę z TypeScriptem - użycie enumów to coś naturalnego: przecież nie będziemy używać jakichś magic numberów! Jednakże enumy w TS zachowują się nietypowo. Po pierwsze, enum z wartościami liczbowymi pozwala na przypisanie do niego dowolnej liczby… (Co to za enum?!)

enum WeekDay {
  Monday = 1,
  Tuesday = 2,
  Wednesday = 3,
  Thursday = 4,
  Friday = 5,
  Saturday = 6,
  Sunday = 6,
}

const wtf: WeekDay = 11 // <-- nie ma błędu!

Po drugie, jeśli do deklaracji enuma dodamy const (a więc const enum), to wszystkie jego użycia zostaną potraktowane jak const w C# - w czasie transpilacji zostaną podmienione na wartości liczbowe.

Po trzecie, enumy z wartościami stringowymi mają pewne pułapki: jeśli przypiszemy enuma do zmiennej, to zmiast typu string, otrzyma on typ literałowy, więc nie będziemy mogli go nadpisać, np.:

enum Status {
  Pending = "pending",
  Done = "done",
}

let statusText = Status.pending
statusText = `Status is: ${statusText}` // <-- tu będzie błąd: "Type 'string' is not assignable to type 'Status'."

Zamiast enuma lepiej użyć tutaj “unię typów literałowych”, czyli:

type Status = "pending" | "done"

Parametry konstruktora jako pola

TypeScript umożliwia zrobienie takie skrótowego zapisu:

class Person {
  constructor(public firstName: string, public lastName: string) {}
}

zamiast rozwlekłego:

class Person {
  public firstName: stirng
  public lastName: string
  constructor(firstName: string, lastName: string) {
    this.firstName = firstName
    this.lastName = lastName
  }
}

Ten syntactic sugar jest dla TS wyjątkowy, bo właśnie też skutkuje generowanie kodu JS - czyli znów wyłamujemy się z reguły “TS minus typy to JS”. Autor książki, o której wspominam poniżej, krytykuje używanie tego “myku” w TS (właśnie z takich powodów, nazwijmy to, kanonicznych), jednak dla mnie jest to bardzo wygodne. Ponadto, podobną propozycję widziałem w którejś z przyszłych/eksperymentalnych wersji C#, więc być może to jest też jakiś trend wspólny dla różnych języków. :)

Ten wpis jest częścią serii o TypeScripcie, inspirowaną lekturą książki D. Vanderkama pt. “TypeScript. Skuteczne programowanie”.


Robert Skarżycki - zdjęcie profilowe

Pisanina, której autorem jest Robert Skarżycki - programista .NET, mąż szczęśliwej żony, rodzic
moje bio
mój Twitter
mój LinkedIn
moje szkolenia i warsztaty

© 2022, Built with Gatsby & passion