Typographie: comportement malsain ou indulgences de fiabilité

Le but est de montrer où TS donne l'illusion de sécurité, vous permettant d'obtenir des erreurs pendant l'exécution du programme.

Nous ne parlerons pas de bogues, dans TS il y a assez de
1 500 bogues ouverts et 6 000 fermés ('est: le problème est: étiquette ouverte: Bogue').

Tous les exemples seront considérés avec:

  • TS strict mode is on (a écrit un article en comprenant)
  • Sans "any" explicite: "as any", "Objects", "Function", {[key: string]: unknown}
  • Sans "any" implicite: (noImplicitAny): importations non typées (fichiers JS purs), inférence de type incorrecte
  • Sans fausses suppositions sur les types: réponse du serveur, typage de bibliothèques tierces

Contenu:

  • introduction
  • Types nominaux, types personnalisés - quand les choses semblent identiques mais si différentes
  • Variance de type, types exacts - sur la relation entre les types
  • Invalidation du raffinement - parler de confiance
  • Exceptions - vaut-il la peine d'admettre quand il est foiré?
  • Opérations dangereuses - la confiance n'est pas toujours bonne
  • Cas bonus - Vérification de type à l'étape de l'examen des relations publiques
  • Conclusion

introduction


Est-il difficile d'écrire une fonction pour ajouter deux nombres dans JS? Prenez une implémentation naïve

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

Vérifions notre implémentation de `sum (2, 2) === 4`, tout semble fonctionner? Pas vraiment, quand nous décrivons une fonction, nous devons penser à toutes sortes de valeurs d'entrée, ainsi qu'à ce que la fonction peut renvoyer

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'

La solidité est la capacité de l'analyseur à prouver qu'il n'y a pas d'erreur pendant l'exécution du programme. Si le programme a été accepté par l'analyseur, il est garanti qu'il est sûr.

Un programme sûr est un programme qui peut fonctionner indéfiniment sans erreurs. Ceux. le programme ne plantera pas ou ne lancera pas d'erreurs.

Programme correct - un programme qui fait ce qu'il doit et ne fait pas ce qu'il ne devrait pas faire. L'exactitude dépend de l'exécution de la logique métier.

Les types peuvent prouver que le programme dans son ensemble est sûr, et teste que le programme est sûr et correct uniquement dans les données de test (couverture à 100%, absence de «mutants» de stryker, réussite d'un test basé sur la propriété et ainsi de suite ne peut rien prouver, et les licences réduit les risques). Il existe des légendes selon lesquelles les prouveurs de théorèmes peuvent prouver l'exactitude du programme.

Il est important de comprendre la philosophie TS, de comprendre ce que l'outil essaie de résoudre et, ce qui est important, ce qu'il n'essaie pas de résoudre.

Une note sur Soundness
TS ignore certaines opérations qui ne sont pas sûres au stade de la compilation. Les endroits au comportement malsain ont été soigneusement pensés.

Objectifs de conception
Pas l'objectif de TS - Pour créer un système de type avec une garantie de sécurité, se concentrer plutôt sur l'équilibre entre sécurité et productivité

Exemple de structure:
Le problème n'est pas un comportement sûr, la liste n'est peut-être pas complète, c'est ce que j'ai trouvé dans des articles, des rapports, Problèmes TS git.
La proposition est une question TS ouverte il y a 3-4 ans, avec un tas de commentaires et d'explications intéressantes par les auteurs
Astuce - À mon humble avis de l'auteur, ce que l'auteur considère comme de bonnes pratiques

Typage structurel vs nominal


Typage structurel vs nominal 1. Problème


Typage structurel - lorsque la comparaison des types ne prend pas en compte leurs noms ou l'endroit où ils ont été déclarés, et les types sont comparés en fonction de la "structure".

Nous voulons envoyer la lettre `sendEmail` à la bonne adresse` ValidatedEmail`, il y a une fonction pour vérifier l'adresse` validateEmail` qui retourne la bonne adresse` ValidatedEmail`. Malheureusement, TS vous permet d'envoyer n'importe quelle chaîne à «sendEmail», car `ValidatedEmail` pour TS n'est pas différent de` 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");

Typage structurel vs nominal 1. Offre


github.com/microsoft/TypeScript/issues/202
Saisissez le mot-clé `nominal` pour que les types soient vérifiés nominalement. Maintenant, nous pouvons interdire de passer juste `string` où` ValidatedEmail` est attendu

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');

Typage structurel vs nominal 1. Conseil


Nous pouvons créer un type `Opaque`, qui prendra du` T` et lui donnera un caractère unique en le combinant avec un type créé à partir du `K` passé. `K` peut être soit un symbole unique (` symbole unique`) soit une chaîne (il faudra alors s'assurer que ces chaînes sont uniques).

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');

Typage structurel vs nominal 2. Problème


Nous avons une classe Dollar et Euro, chacune des classes a une méthode d'ajout pour ajouter le dollar au dollar et l'euro à l'euro. Pour TS, ces classes sont structurellement égales et nous pouvons ajouter le dollar à l'euro.

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);

Typage structurel vs nominal 2. Offre


github.com/microsoft/TypeScript/issues/202
La phrase est tout de même, avec `nominal`, mais puisque Comme les classes peuvent magiquement devenir nominales (plus à ce sujet plus tard), les possibilités de faire une telle transformation d'une manière plus explicite sont considérées.

Typage structurel vs nominal 1. Conseil


Si la classe a un champ privé (natif avec `#` ou de TS c `private`), alors la classe devient par magie Nominal, le nom et la valeur peuvent être n'importe quoi. Le `!` (Assertion d'affectation définie) est utilisé pour empêcher TS de jurer sur un champ non initialisé (strictNullChecks, les drapeaux strictPropertyInitialization sont activés).

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);

Variance de type 1. Problème


Une option de programmation, en bref, est la possibilité de passer Supertype / Subtype là, où Type est attendu. Par exemple, il existe une hiérarchie Forme -> Cercle -> Rectangle, est-il possible de transférer ou de renvoyer une Forme / Rectangle si un Cercle est attendu?

Variante de programmation habr , SO .

Nous pouvons passer le type avec le champ dans lequel se trouve le nombre à une fonction qui attend le champ sous forme de chaîne ou de nombre, et mute l'objet transmis dans le corps, en changeant le champ en chaîne. Ceux. `{status: number} as {status: number | string} as {status: string} `voici une astuce comme transformer un nombre en chaîne, provoquant une erreur surprise .

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());

Variation de type 1. Offre


github.com/Microsoft/TypeScript/issues/10717
Il est proposé d'introduire «in / out» pour limiter explicitement la covariance / contravariance pour les génériques.

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());

Variance de type 1. Conseil


Si nous travaillons avec des structures immuables, il n'y aura pas une telle erreur (nous avons déjà activé l'indicateur strictFunctionTypes).

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());

Variation de type 1. Bonus


Readonly est attribuable à mutable
github.com/Microsoft/TypeScript/issues/13347
github.com/microsoft/TypeScript/pull/6532#issuecomment-174356151

Mais, même si nous avons créé le type Readonly, TS n'interdira pas de passer à la fonction où non attendu Readonly `Readonly <{readonly status: number}> as {status: number | chaîne} comme {état: chaîne} `

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());

Variance de type 2. Problème


Les objets peuvent contenir des champs supplémentaires que les types qui leur correspondent n'ont pas: `{message: string; status: string} as {message: string} `. En raison de quoi certaines opérations peuvent ne pas être sûres

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 pensait qu'à la suite de la fusion, `{... {message: string, status: number}, ... {message: string}}` status sera un nombre.

En réalité, `{... {message:" Not found ", statut: 404}, ... {message:" No data ", statut:" NotFound "},}` status - string.

Variation de type 2. Offre


github.com/microsoft/TypeScript/issues/12936
Présentation d'un type `exact 'ou d'une syntaxe similaire pour dire qu'un type ne peut pas contenir de champs supplémentaires.

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);

Variance de type 2. Conseil


Fusionner des objets en répertoriant explicitement les champs ou en filtrant les champs inconnus.

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);

Invalidation du raffinement. Problème


Après avoir prouvé quelque chose sur l'état externe, appeler des fonctions n'est pas sûr, car Il n'y a aucune garantie que les fonctions ne modifient pas cet état externe:

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);
}

Invalidation du raffinement. Phrase


github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251
Ajoutez le modificateur `pure` pour les fonctions, cela vous permettra au moins de faire confiance à ces fonctions

Invalidation du raffinement. Pointe


Utilisez des structures de données immuables, alors l'appel de fonction sera a priori sûr pour les vérifications précédentes.

Prime


Le type de flux est si puissant qu'il n'a pas tous les problèmes énumérés ci-dessus, mais il est tellement en cours d'exécution que je ne recommanderais pas de l'utiliser.

Exceptions. Problème


TS n'aide en rien à travailler avec les exceptions; rien n'est clair sur la signature de la fonction.

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);

Exceptions. Phrase


github.com/microsoft/TypeScript/issues/13219
Il est proposé d'introduire une syntaxe qui permet de décrire explicitement les exceptions dans une signature de fonction

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));

Exceptions. Prime


github.com/microsoft/TypeScript/issues/6283
Pour une raison quelconque, dans TS, la définition de type pour Promise ignore le type d'erreur

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>;
}

Exceptions. Pointe


Prenez un conteneur Soit comme Promise, avec seulement la meilleure frappe. ( Soit exemple d'implémentation )

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));

Opérations dangereuses. Problème


Si nous avons un tuple de taille fixe, alors TS peut garantir qu'il y a quelque chose sur l'index demandé. Cela ne fonctionnera pas pour la baie et TS nous fera confiance

// 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());

Opérations dangereuses. Phrase


github.com/microsoft/TypeScript/issues/13778
Il est proposé d'ajouter «non défini» au type de retour «T» pour l'accès d'index au tableau. Mais dans ce cas, lors de l'accès à n'importe quel index, vous devrez utiliser «?» Ou effectuer des vérifications explicites.

// 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());
}

Opérations dangereuses. Pointe


Afin de ne pas produire d'entités au-delà du besoin, nous prenons le conteneur précédemment connu `Soit` et écrivons une fonction sûre pour travailler avec l'index, qui renverra` Soit <null, T> `.

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()));

Prime Fonctions de rechargement


Si nous voulons dire qu'une fonction prend quelques lignes et retourne une chaîne ou prend quelques nombres et retourne un nombre, alors dans l'implémentation ces signatures seront contiguës, et le programmeur devrait garantir leur exactitude, mais sur TS.

PS regarde l'alternative à travers des types génériques et conditionnels:

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();


Prime Type garde


TS fait confiance au programmeur que «isSuperUser» détermine correctement qui est «SuperUser» et si «Vasya» est ajouté, il n'y aura pas d'invite.

PS mérite réflexion sur la façon dont nous allons distinguer les types déjà au stade de leur union - tagged union

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(","));
  }
}

Conclusions sur les conseils


- Types nominaux : Type opaque, champs privés
- Variance de type : Types exacts, Type DeepReadonly
- Exceptions : Soit monade
- Invalidation de raffinement : Fonctions pures
- Opérations dangereuses (accès à l'index) : Soit / Peut-être monades

Données immuables, fonctions pures, monades ... Félicitations , nous avons prouvé que FP est cool!

résultats


  • TS veut trouver un équilibre entre l'exactitude et la productivité
  • Les tests ne peuvent prouver la sécurité et l'exactitude que pour les données de test.
  • Les types peuvent prouver la sécurité globale d'un programme.
  • Mutation - mauvaise, d'accord?

Comme DZ recommanderait de jouer avec Flow après avoir corrigé une simple erreur:

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

Exemple de code, solution et analyse du problème, liens utiles dans le référentiel .

All Articles