Typoskript: fehlerhaftes Verhalten oder Ablässe der Zuverlässigkeit

Das Ziel ist es zu zeigen, wo TS die Illusion von Sicherheit vermittelt, sodass Sie Fehler erhalten können, während das Programm ausgeführt wird.

Wir werden nicht über Bugs sprechen, in TS gibt es genug
1.500 offene Bugs und 6.000 geschlossene ('is: issue is: open label: Bug').

Alle Beispiele werden betrachtet mit:

  • Der strikte TS- Modus ist aktiviert (schrieb einen Artikel, während er verstand)
  • Ohne explizites "any": "as any", "Objects", "Function", {[key: string]: unknown}
  • Ohne implizites "any": (noImplicitAny): untypisierte Importe (reine JS-Dateien), falsche Typinferenz
  • Ohne falsche Vermutungen über Typen: Antwort vom Server, Eingabe von Bibliotheken von Drittanbietern

Inhalt:

  • Einführung
  • Nominaltypen, benutzerdefinierte Typen - wenn die Dinge gleich, aber so unterschiedlich erscheinen
  • Typvarianz, genaue Typen - über die Beziehung zwischen Typen
  • Verfeinerung ungültig machen - über Vertrauen sprechen
  • Ausnahmen - lohnt es sich zuzugeben, wenn man durcheinander ist?
  • Unsichere Operationen - Vertrauen ist nicht immer gut
  • Bonusfälle - Typprüfung in der PR-Überprüfungsphase
  • Fazit

Einführung


Ist es schwierig, eine Funktion zum Hinzufügen von zwei Zahlen in JS zu schreiben? Nehmen Sie eine naive Implementierung

function sum(a, b) {
	return a + b;
}

Lassen Sie uns unsere Implementierung von `sum (2, 2) === 4` überprüfen. Scheint alles zu funktionieren? Nicht wirklich, wenn wir eine Funktion beschreiben, sollten wir über alle Arten von Eingabewerten nachdenken sowie darüber, was die Funktion zurückgeben kann

1.1 + 2.7   // 3.8000000000000003
NaN + 2     // NaN
99999999999999992 + 99999999999999992 // 200000000000000000
2n + 2      // Uncaught TypeError: 
            // Cannot mix BigInt and other types, use explicit conversions.
{} + true   // 1
2 + '2'     // '22'

Solidität ist die Fähigkeit des Analysators, zu beweisen, dass während der Ausführung des Programms keine Fehler vorliegen. Wenn das Programm vom Analysegerät akzeptiert wurde, ist es garantiert sicher.

Sicheres Programm ist ein Programm, das für immer ohne Fehler arbeiten kann. Jene. Das Programm wird nicht abstürzen oder Fehler auslösen.

Richtiges Programm - ein Programm, das tut, was es sollte und nicht tut, was es nicht sollte. Die Richtigkeit hängt von der Ausführung der Geschäftslogik ab.

Typen können beweisen, dass das Programm als Ganzes sicher ist, und Tests, dass das Programm nur innerhalb der Testdaten sicher und korrekt ist (100% Abdeckung, das Fehlen von „Mutanten“ von Stryker, Bestehen eines eigenschaftsbasierten Tests usw. können nichts beweisen, und Lizenzen reduziert Risiken). Es gibt Legenden, nach denen Theorembeweiser die Richtigkeit des Programms beweisen können.

Es ist wichtig, die TS-Philosophie zu verstehen, zu verstehen, was das Tool zu lösen versucht und was wichtig ist, was es nicht zu lösen versucht.

Ein Hinweis zu Soundness
TS überspringt einige Vorgänge, die bei der Kompilierung nicht sicher sind. Orte mit schlechtem Verhalten wurden sorgfältig durchdacht.

Entwurfsziele
Nicht das Ziel für TS - Um ein Typensystem mit einer Sicherheitsgarantie zu erstellen, konzentrieren Sie sich stattdessen auf das Gleichgewicht zwischen Sicherheit und Produktivität.

Beispielstruktur:
Das Problem ist kein sicheres Verhalten. Die Liste ist möglicherweise nicht vollständig. Dies habe ich in Artikeln, Berichten und TS Git Probleme.
Der Vorschlag ist eine TS-Ausgabe, die vor drei bis vier Jahren geöffnet wurde und eine Reihe von Kommentaren und interessanten Erklärungen der Autoren enthält.
Tipp - IMHO des Autors, was der Autor als bewährte Verfahren ansieht

Strukturelle vs nominelle Typisierung


Strukturelle vs. nominelle Typisierung 1. Problem


Strukturelle Typisierung - Beim Vergleich von Typen werden deren Namen oder Deklarationsorte nicht berücksichtigt, und Typen werden gemäß der "Struktur" verglichen.

Wir möchten den Brief "sendEmail" an die richtige Adresse "ValidatedEmail" senden. Es gibt eine Funktion zum Überprüfen der Adresse "validateEmail", die die richtige Adresse "ValidatedEmail" zurückgibt. Leider können Sie mit TS eine beliebige Zeichenfolge an "sendEmail" senden, da `ValidatedEmail` für TS unterscheidet sich nicht von` string`

type ValidatedEmail = string;
declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail("asdf@gmail.com"));

// Should be error!
sendEmail("asdf@gmail.com");

Strukturelle vs nominelle Typisierung 1. Angebot


github.com/microsoft/TypeScript/issues/202
Geben Sie das Schlüsselwort "nominal" ein, damit die Typen "Nominal" aktiviert werden. Jetzt können wir verbieten, nur "string" zu übergeben, wo "ValidatedEmail" erwartet wird

nominal type ValidatedEmail = string;
declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail('asdf@gmail.com'));

// Error!
sendEmail('asdf@gmail.com');

Strukturelle oder nominelle Eingabe 1. Tipp


Wir können einen "undurchsichtigen" Typ erstellen, der etwas "T" nimmt und ihm Einzigartigkeit verleiht, indem wir ihn mit einem Typ kombinieren, der aus dem übergebenen "K" erstellt wurde. `K` kann entweder ein eindeutiges Symbol (` eindeutiges Symbol`) oder eine Zeichenfolge sein (dann muss sichergestellt werden, dass diese Zeichenfolgen eindeutig sind).

type Opaque<K extends symbol | string, T> 
	= T & { [X in K]: never };

declare const validatedEmailK: unique symbol;
type ValidatedEmail = Opaque<typeof validatedEmailK, string>;
// type ValidatedEmail = Opaque<'ValidatedEmail', string>;

declare function validateEmail(email: string): ValidatedEmail;

declare function sendEmail(mail: ValidatedEmail): void;
sendEmail(validateEmail('asdf@gmail.com'));

// Argument of type '"asdf@gmail.com"' is not assignable
//  to parameter of type 'Opaque<unique symbol, string>'.
sendEmail('asdf@gmail.com');

Strukturelle vs. nominelle Typisierung 2. Problem


Wir haben eine Dollar- und eine Euro-Klasse. Jede der Klassen hat eine Additionsmethode zum Addieren des Dollars zum Dollar und des Euro zum Euro. Für TS sind diese Klassen strukturell gleich und wir können den Dollar zum Euro hinzufügen.

export class Dollar {
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(dollar: Dollar): Dollar {
    return new Dollar(dollar.value + this.value);
  }
}

class Euro {
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(euro: Euro): Euro {
    return new Euro(euro.value + this.value);
  }
}

const dollars100 = new Dollar(100);
const euro100 = new Euro(100);

// Correct
dollars100.add(dollars100);
euro100.add(euro100);

// Should be error!
dollars100.add(euro100);

Strukturelle vs nominelle Typisierung 2. Angebot


github.com/microsoft/TypeScript/issues/202
Der Satz ist alle gleich, mit "nominal", aber seitdem Da Klassen auf magische Weise nominal werden können (dazu später mehr), werden die Möglichkeiten einer expliziteren Transformation in Betracht gezogen.

Strukturelle oder nominelle Eingabe 1. Tipp


Wenn die Klasse ein privates Feld hat (nativ mit "#" oder von TS c "privat"), wird die Klasse auf magische Weise zu "Nominal". Der Name und der Wert können beliebig sein. Das `!` (Definite Assignment Assertion) wird verwendet, um zu verhindern, dass TS auf ein nicht initialisiertes Feld schwört (strictNullChecks, strictPropertyInitialization-Flags sind aktiviert).

class Dollar {
  // #desc!: never;
  private desc!: never;
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(dollar: Dollar) {
    return new Dollar(dollar.value + this.value);
  }
}

class Euro {
  // #desc!: never;
  private desc!: never;
  value: number;

  constructor(value: number) {
    this.value = value;
  }

  add(euro: Euro) {
    return new Euro(euro.value + this.value);
  }
}

const dollars100 = new Dollar(100);
const euro100 = new Euro(100);

// Correct
dollars100.add(dollars100);
euro100.add(euro100);

// Error: Argument of type 'Euro' is not assignable to parameter of type 'Dollar
dollars100.add(euro100);

Typvarianz 1. Problem


Kurz gesagt, eine Programmieroption ist die Möglichkeit, Supertype / Subtype dort zu übergeben, wo Type erwartet wird. Zum Beispiel gibt es eine Hierarchie Form -> Kreis -> Rechteck. Ist es möglich, eine Form / ein Rechteck zu übertragen oder zurückzugeben, wenn Kreis erwartet wird?

Variante in der Programmierung habr , SO .

Wir können den Typ mit dem Feld, in dem die Zahl liegt, an eine Funktion übergeben, die das Feld als Zeichenfolge oder Zahl erwartet, und das übertragene Objekt im Körper mutieren und das Feld in eine Zeichenfolge ändern. Jene. `{status: number} als {status: number | string} as {status: string} `Hier ist ein Trick wie das Verwandeln einer Zahl in einen String, der einen Überraschungsfehler verursacht.

function changeStatus(arg: { status: number | string }) {
  arg.status = "NotFound";
}

const error: { status: number } = { status: 404 };
changeStatus(error);

// Error: toFixed is not a function
console.log(error.status.toFixed());

Typabweichung 1. Angebot


github.com/Microsoft/TypeScript/issues/10717
Es wird vorgeschlagen, "in / out" einzuführen, um die Kovarianz / Kontravarianz für Generika explizit zu begrenzen.

function changeStatus<
  out T extends {
    status: number | string;
  }
>(arg: T) {
  arg.status = "NotFound";
}

const error: { status: number } = { status: 404 };
// Error!
changeStatus(error);

console.log(error.status.toFixed());

Typvarianz 1. Tipp


Wenn wir mit unveränderlichen Strukturen arbeiten, tritt kein solcher Fehler auf (wir haben das Flag strictFunctionTypes bereits aktiviert).

function changeStatus(arg: Readonly<{ status: number | string }>) {
  // Error: Cannot assign, status is not writable
  arg.status = "NotFound";
}

const error: Readonly<{ status: number }> = { status: 404 };
changeStatus(error);

console.log(error.status.toFixed());

Typvarianz 1. Bonus


Readonly kann dem veränderlichen
github.com/Microsoft/TypeScript/issues/13347
github.com/microsoft/TypeScript/pull/6532#issuecomment-174356151

zugewiesen werden. Selbst wenn wir den Readonly-Typ erstellt haben, wird TS nicht verbieten, an die Funktion zu übergeben, bei der nicht erwartet Readonly `Readonly <{readonly status: number}> als {status: number | Zeichenfolge} als {Status: Zeichenfolge} `

function changeStatus(arg: { status: number | string }) {
  arg.status = "NotFound";
}

const error: Readonly<{ readonly status: number }> 
  = { status: 404 };
changeStatus(error);

// Error: toFixed is not a function
console.log(error.status.toFixed());

Typvarianz 2. Problem


Objekte können zusätzliche Felder enthalten, die die ihnen entsprechenden Typen nicht haben: `{message: string; status: string} as {message: string} `. Aufgrund dessen sind einige Operationen möglicherweise nicht sicher

const error: { message: string; status: string } = {
  message: "No data",
  status: "NotFound"
};

function updateError(arg: { message: string }) {
  const defaultError = { message: "Not found", status: 404 };
  const newError: { message: string; status: number }
    = { ...defaultError, ...arg };
  
  // Error: toFixed is not a function
  console.log(newError.status.toFixed());
}

updateError(error);

TS dachte, dass als Ergebnis der Zusammenführung der Status "{... {Nachricht: Zeichenfolge, Status: Nummer}, ... {Nachricht: Zeichenfolge}}" eine Zahl sein wird.

In Wirklichkeit `{... {Nachricht:" Nicht gefunden ", Status: 404}, ... {Nachricht:" Keine Daten ", Status:" NotFound "},}` Status - Zeichenfolge.

Typabweichung 2. Angebot


github.com/microsoft/TypeScript/issues/12936
Einführung eines "Exact" -Typs oder einer ähnlichen Syntax, um zu sagen, dass ein Typ keine zusätzlichen Felder enthalten kann.

const error: Exact<{ message: string; }> = {
  message: "No data",
};

function updateError(arg: Exact<{ message: string }>) {
  const defaultError = {  message: "Not found", status: 404, };
  // Can spread only Exact type!
  const newError = { ...defaultError, ...arg };
  console.log(newError.status.toFixed());
}

updateError(error);

Typvarianz 2. Tipp


Führen Sie Objekte zusammen, indem Sie Felder explizit auflisten oder unbekannte Felder herausfiltern.

const error: { message: string; status: string } = {
  message: "No data",
  status: "NotFound"
};

function updateError(arg: { message: string }) {
  const defaultError = { message: "Not found", status: 404 };
  // Merge explicitly or filter unknown fields
  const newError = { ...defaultError, message: arg.message };
  console.log(newError.status.toFixed());
}

updateError(error);

Ungültigmachung der Verfeinerung. Problem


Nachdem wir etwas über den externen Zustand bewiesen haben, ist das Aufrufen von Funktionen nicht sicher, weil Es gibt keine Garantie dafür, dass Funktionen diesen externen Zustand nicht ändern:

export function logAge(name: string, age: number) {
  // 2nd call -  Error: toFixed is not a function
  console.log(`${name} will lose ${age.toFixed()}`);
  person.age = "PLACEHOLDER";
}

const person: { name: string; age: number | string } = {
  name: "Person",
  age: 42
};

if (typeof person.age === "number") {
  logAge(person.name, person.age);
  // refinement should be invalidated
  logAge(person.name, person.age);
}

Ungültigmachung der Verfeinerung. Satz


github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251
Fügen Sie den Modifikator "pure" für Funktionen hinzu, damit Sie diesen Funktionen zumindest vertrauen können

Ungültigmachung der Verfeinerung. Trinkgeld


Verwenden Sie unveränderliche Datenstrukturen, dann ist der Funktionsaufruf a priori sicher für frühere Überprüfungen.

Bonus


Der Flusstyp ist so stark, dass er nicht alle oben aufgeführten Probleme aufweist, aber so ausgeführt wird, dass ich die Verwendung nicht empfehlen würde.

Ausnahmen. Problem


TS hilft in keiner Weise, mit Ausnahmen zu arbeiten, da auf der Funktionssignatur nichts klar ist.

import { JokeError } from "../helpers";

function getJoke(isFunny: boolean): string {
  if (isFunny) {
    throw new JokeError("No funny joke");
  }
  return "Duh";
}

const joke: string = getJoke(true);
console.log(joke);

Ausnahmen. Satz


github.com/microsoft/TypeScript/issues/13219
Es wird vorgeschlagen, eine Syntax einzuführen, mit der Ausnahmen in einer Funktionssignatur explizit beschrieben werden können

import { JokeError } from '../helpers';

function getJoke(isFunny: boolean): string | throws JokeError {
  /*...*/}

function getJokeSafe(isFunny: boolean): string {
  try {
    return getJoke(isFunny);
  } catch (error) {
    if (error instanceof JokeError) {
      return "";
    } else {
      // Should infer error correctly, should cast to never
      return error as never;
    }
  }
}

console.log(getJokeSafe(true));

Ausnahmen. Bonus


github.com/microsoft/TypeScript/issues/6283
Aus irgendeinem Grund ignoriert die Typdefinition für Promise in TS die Art des Fehlers

const promise1: Promise<number> = Promise.resolve(42);

const promise: Promise<never> = Promise.reject(new TypeError());

// typescript/lib
interface PromiseConstructor {
  new <T>(
    executor: (
      resolve: (value?: T | PromiseLike<T>) => void,
      reject: (reason?: any) => void
    ) => void
  ): Promise<T>;
}

Ausnahmen. Trinkgeld


Nehmen Sie einen der beiden Container wie Promise mit nur der besten Eingabe. ( Entweder Implementierungsbeispiel )

import { Either, exhaustiveCheck, JokeError } from "../helpers";

function getJoke(isFunny: boolean): Either<JokeError, string> {
  if (isFunny) {
    return Either.left(new JokeError("No funny joke"));
  }
  return Either.right("Duh");
}

getJoke(true)
  // (parameter) error: JokeError
  .mapLeft(error => {
    if (error instanceof JokeError) {
      console.log("JokeError");
    } else {
      exhaustiveCheck(error);
    }
  })
  // (parameter) joke: string
  .mapRight(joke => console.log(joke));

Unsichere Operationen. Problem


Wenn wir ein Tupel fester Größe haben, kann TS garantieren, dass der angeforderte Index etwas enthält. Dies funktioniert nicht für das Array und TS wird uns vertrauen

// ReadOnly fixed size tuple
export const constNumbers: readonly [1, 2, 3] 
  = [1, 2, 3] as const;

// Error: Object is possibly 'undefined'.
console.log(constNumbers[100].toFixed());

const dynamicNumbers: number[] = [1, 2, 3];
console.log(dynamicNumbers[100].toFixed());

Unsichere Operationen. Satz


github.com/microsoft/TypeScript/issues/13778
Es wird vorgeschlagen, dem Rückgabetyp "T" für den Indexzugriff auf das Array "undefined" hinzuzufügen. In diesem Fall müssen Sie jedoch beim Zugriff auf einen beliebigen Index `?` Verwenden oder explizite Überprüfungen durchführen.

// interface Array<T> {
//   [n: number]: T | undefined;
// }

const dynamicNumbers: number[] = [1, 2, 3];
// Error: Object is possibly 'undefined'.
console.log(dynamicNumbers[100].toFixed());

// Optional chaining `?`
console.log(dynamicNumbers[100]?.toFixed());

// type refinement
if (typeof dynamicNumbers[100] === 'number') {
  console.log(dynamicNumbers[100].toFixed());
}

Unsichere Operationen. Trinkgeld


Um keine Entitäten zu erzeugen, die über den Bedarf hinausgehen, nehmen wir den zuvor bekannten Container "Entweder" und schreiben eine sichere Funktion für die Arbeit mit dem Index, die "Entweder <null, T>" zurückgibt.

import { Either } from "../helpers";

function safeIndex<T>(
  array: T[],
  index: number,
): Either<null, T> {
  if (index in array) {
    return Either.right(array[index]);
  }
  return Either.left(null);
}

const dynamicNumbers: number[] = [1, 2, 3];

safeIndex(dynamicNumbers, 100)
  .mapLeft(() => console.log("Nothing"))
  .mapRight(el => el + 2)
  .mapRight(el => console.log(el.toFixed()));

Bonus Funktionen zum Nachladen


Wenn wir sagen wollen, dass eine Funktion ein paar Zeilen benötigt und eine Zeichenfolge zurückgibt oder ein paar Zahlen und eine Zahl zurückgibt, sind diese Signaturen in der Implementierung zusammenhängend, und der Programmierer sollte ihre Richtigkeit garantieren, jedoch auf TS.

PS betrachten die Alternative durch generische und bedingte Typen:

function add(a: string, b: string): string;
function add(a: number, b: number): number;
function add(a: string | number,
             b: string | number,
): string | number {
  return `${a} + ${b}`;
}

const sum: number = add(2, 2);
// Error: toFixed is not a function
sum.toFixed();


Bonus Typ Wache


TS vertraut dem Programmierer, dass "isSuperUser" korrekt bestimmt, wer "SuperUser" ist, und wenn "Vasya" hinzugefügt wird, werden keine Eingabeaufforderungen angezeigt.

PS ist es wert, darüber nachzudenken, wie wir Typen unterscheiden können, die sich bereits in der Phase ihrer gewerkschaftlich gekennzeichneten Vereinigung befinden

type SimpleUser = { name: string };
type SuperUser = { 
  name: string; 
  isAdmin: true; 
  permissions: string[] 
};
type Vasya = { name: string; isAdmin: true; isGod: true };
type User = SimpleUser | SuperUser | Vasya;

function isSuperUser(user: User): user is SuperUser {
  return "isAdmin" in user && user.isAdmin;
}

function doSomethings(user: User) {
  // Error: Cannot read property 'join' of undefined
  if (isSuperUser(user)) {
    console.log(user.permissions.join(","));
  }
}

Schlussfolgerungen zu den Tipps


- Nominaltypen : Opaker Typ, private Felder
- Typvarianz: Exakte Typen, DeepReadonly-Typ
- Ausnahmen : Entweder Monade
- Verfeinerung ungültig machen : Reine Funktionen
- Unsichere Operationen ( Indexzugriff ) : Entweder / Vielleicht Monaden

Unveränderliche Daten, reine Funktionen, Monaden ... Herzlichen Glückwunsch haben wir bewiesen, dass FP cool ist!

Ergebnisse


  • TS möchte ein Gleichgewicht zwischen Korrektheit und Produktivität herstellen
  • Tests können Sicherheit und Richtigkeit nur für Testdaten nachweisen.
  • Typen können die Gesamtsicherheit eines Programms belegen.
  • Mutation - schlecht, okay?

Da DZ empfehlen würde, mit Flow zu spielen, nachdem ein einfacher Fehler korrigiert wurde:

// https://flow.org/try/
declare function log(arg: { name: string, surname?: string }): void;
const person: { name: string } =  { name: 'Negasi' };
// Error
log(person);

Beispielcode, Lösung und Analyse des Problems, nützliche Links im Repository .

All Articles