Texto datilografado: comportamento doentio ou indulgências de confiabilidade

O objetivo é mostrar onde o TS fornece a ilusão de segurança, permitindo que você obtenha erros enquanto o programa está em execução.

Não falaremos sobre bugs, no TS existem
1.500 bugs abertos e 6.000 fechados ('is: issue is: open label: Bug').

Todos os exemplos serão considerados com:

  • O modo estrito de TS está ativado (escreveu um artigo ao entender)
  • Sem "any": "as any", "Objects", "Function", {[key: string]: unknown}
  • Sem "any" implícito: (noImplicitAny): importações sem tipo (arquivos JS puros), inferência de tipo incorreta
  • Sem suposições falsas sobre tipos: resposta do servidor, digitação de bibliotecas de terceiros

Conteúdo:

  • Introdução
  • Tipos nominais, tipos personalizados - quando as coisas parecem iguais, mas tão diferentes
  • Variação de tipo, tipos exatos - sobre o relacionamento entre tipos
  • Invalidação de refinamento - fale sobre confiança
  • Exceções - vale a pena admitir quando erra?
  • Operações inseguras - a confiança nem sempre é boa
  • Casos de bônus - verificação de tipo na fase de revisão de relações públicas
  • Conclusão

Introdução


É difícil escrever uma função para adicionar dois números em JS? Tome uma implementação ingênua

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

Vamos verificar nossa implementação de `sum (2, 2) === 4`, tudo parece funcionar? Na verdade, quando descrevemos uma função, devemos pensar em todos os tipos de valores de entrada, bem como no que a função pode retornar

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'

Solidez é a capacidade do analisador de provar que não há erros durante a operação do programa. Se o programa foi aceito pelo analisador, é garantido que ele é seguro.

Programa seguro é um programa que pode funcionar para sempre sem erros. Essa. o programa não trava ou gera erros.

Programa correto - um programa que faz o que deveria e não faz o que não deveria. A correção depende da execução da lógica de negócios.

Os tipos podem provar que o programa como um todo é seguro, e testa se o programa é seguro e correto apenas nos dados do teste (cobertura de 100%, ausência de "mutantes" do stryker, passar em um teste baseado na propriedade e assim por diante não pode provar nada, e licenças reduz riscos). Existem lendas de que os provadores de teoremas podem provar a correção do programa.

É importante entender a filosofia TS, entender o que a ferramenta está tentando resolver e, o que é importante, o que não está tentando resolver.

Uma observação no Soundness
TS ignora algumas operações que não têm certeza no estágio de compilação. Lugares com comportamento doentio foram cuidadosamente pensados.

Objetivos do projeto
Não é o objetivo do TS - Para criar um sistema de tipos com garantia de segurança, concentre-se no equilíbrio entre segurança e produtividade.Estrutura de

exemplo:
o problema não é um comportamento seguro, a lista pode não estar completa, foi o que encontrei em artigos, relatórios, Problemas de TS git.
A proposta é uma questão de TS aberta há 3 a 4 anos, com vários comentários e explicações interessantes dos autores.Tipo
- IMHO do autor, o que o autor considera boas práticas

Tipagem estrutural vs nominal


Tipagem estrutural vs nominal 1. Problema


Tipagem estrutural - ao comparar tipos não leva em consideração seus nomes ou onde foram declarados, e os tipos são comparados de acordo com a "estrutura".

Queremos enviar a letra "sendEmail" para o endereço correto "ValidatedEmail", existe uma função para verificar o endereço "validateEmail" que retorna o endereço correto "ValidatedEmail". Infelizmente, o TS permite que você envie qualquer string para `sendEmail`, porque `ValidatedEmail` para TS não é diferente 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");

Tipagem estrutural x nominal 1. Oferta


github.com/microsoft/TypeScript/issues/202
Digite a palavra-chave `nominal 'para que os tipos sejam verificados nominalmente. Agora podemos proibir a passagem apenas de `string` onde é esperado o` ValidatedEmail`

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

Tipagem estrutural vs nominal 1. Dica


Nós podemos criar um tipo `opaco ', que pegará um pouco de' T` e lhe dará exclusividade, combinando-o com um tipo criado a partir do` K` passado. `K` pode ser um símbolo único (` símbolo exclusivo`) ou uma string (então será necessário garantir que essas strings sejam únicas).

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

Tipagem estrutural x nominal 2. Problema


Temos uma classe de dólar e euro, cada uma das classes tem um método de adição para adicionar o dólar ao dólar e o euro ao euro. Para TS, essas classes são estruturalmente iguais e podemos adicionar o dólar ao 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);

Tipagem estrutural vs nominal 2. Oferta


github.com/microsoft/TypeScript/issues/202
A frase é a mesma, com `nominal`, mas desde que Como as classes podem magicamente se tornar Nominais (mais sobre isso mais tarde), são consideradas as possibilidades de fazer essa transformação de uma maneira mais explícita.

Tipagem estrutural vs nominal 1. Dica


Se a classe tiver um campo privado (nativo com `#` ou de TS c `private`), a classe magicamente se tornará nominal, o nome e o valor podem ser qualquer coisa. O `!` (Asserção de atribuição definida) é usado para impedir que o TS jure em um campo não inicializado (strictNullChecks, os flags strictPropertyInitialization estão ativados).

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

Variação de tipo 1. Problema


Uma opção de programação, em resumo, é a capacidade de passar Supertype / Subtype para lá, onde Type é esperado. Por exemplo, existe uma hierarquia Shape -> Circle -> Rectangle, é possível transferir ou retornar uma Shape / Rectangle se Circle for esperado?

Variante na programação habr , SO .

Podemos passar o tipo com o campo no qual o número está para uma função que espera o campo como uma sequência ou um número e modificar o objeto transmitido no corpo, alterando o campo para uma sequência. Essa. `{status: number} como {status: number | string} como {status: string} `aqui é um truque como transformar um número em uma string, causando um erro surpresa .

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

Variação de tipo 1. Oferta


github.com/Microsoft/TypeScript/issues/10717
É proposto introduzir `in / out` para limitar explicitamente a covariância / contravariância para genéricos.

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

Variação de tipo 1. Dica


Se trabalharmos com estruturas imutáveis, não haverá esse erro (já ativamos o sinalizador 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());

Variação de tipo 1. Bônus


Readonly é atribuível a mutable
github.com/Microsoft/TypeScript/issues/13347
github.com/microsoft/TypeScript/pull/6532#issuecomment-174356151

Mas, mesmo que tenhamos criado o tipo Readonly, o TS não proibirá passar para a função em que não esperado Readonly `Readonly <{status somente leitura: número}> como {status: número | string} como {status: string} `

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

Variação de tipo 2. Problema


Os objetos podem conter campos adicionais que os tipos correspondentes a eles não possuem: `{message: string; status: string} como {message: string} `. Devido ao qual algumas operações podem não ser seguras

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

O TS considerou que, como resultado da mesclagem, o status `{... {message: string, status: number}, ... {message: string}}` será um número.

Na realidade, `{... {message:" Not found ", status: 404}, ... {message:" No data ", status:" NotFound "},}` status - string.

Variação de tipo 2. Oferta


github.com/microsoft/TypeScript/issues/12936
Introduzindo um tipo `Exato` ou sintaxe semelhante para dizer que um tipo não pode conter campos adicionais.

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

Tipo de variação 2. Dica


Mesclar objetos listando explicitamente campos ou filtrando campos desconhecidos.

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

Invalidação de refinamento. Problema


Depois de provarmos algo sobre o estado externo, chamar funções não é seguro, porque Não há garantias de que as funções não alterem este estado externo:

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

Invalidação de refinamento. Frase


github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251
Adicione o modificador `pure` para funções, isso permitirá pelo menos confiar nessas funções

Invalidação de refinamento. Gorjeta


Use estruturas de dados imutáveis, a chamada de função será a priori segura para verificações anteriores.

Bônus


O tipo de fluxo é tão forte que não possui todos os problemas listados acima, mas é tão eficiente que eu não recomendaria usá-lo.

Exceções. Problema


O TS não ajuda a trabalhar com exceções de forma alguma; nada fica claro na assinatura da função.

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

Exceções. Frase


github.com/microsoft/TypeScript/issues/13219
É proposto introduzir uma sintaxe que permita descrever explicitamente exceções em uma assinatura de função

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

Exceções. Bônus


github.com/microsoft/TypeScript/issues/6283
Por algum motivo, no TS, a definição de tipo para Promise ignora o tipo de erro

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

Exceções. Gorjeta


Pegue um container Either como Promise, com apenas a melhor digitação. ( Exemplo de implementação )

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

Operações inseguras. Problema


Se tivermos uma tupla de tamanho fixo, o TS poderá garantir que exista algo no índice solicitado. Isso não funcionará para a matriz e o TS confiará em nós

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

Operações inseguras. Frase


github.com/microsoft/TypeScript/issues/13778
Propõe-se adicionar `indefinido` ao tipo de retorno` T` para acesso do índice à matriz. Mas neste caso, ao acessar em qualquer índice, você terá que usar `?` Ou fazer verificações explícitas.

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

Operações inseguras. Gorjeta


Para não produzir entidades além da necessidade, pegamos o contêiner anteriormente conhecido `Either 'e escrevemos uma função segura para trabalhar com o índice, que retornará` Ou <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()));

Bônus Recarregando funções


Se quisermos dizer que uma função pega algumas linhas e retorna uma string ou pega alguns números e retorna um número, na implementação essas assinaturas serão contíguas, e o programador deve garantir sua correção, mas no TS.

O PS olha a alternativa através de tipos genéricos e condicionais:

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


Bônus Protetor de tipo


O TS confia no programador que `isSuperUser` determina corretamente quem é` SuperUser` e, se `Vasya` for adicionado, não haverá avisos.

PS vale a pena pensar em como vamos distinguir tipos que já estão no estágio de sua união - union tagged

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

Conclusões sobre as dicas


- Tipos nominais : tipo opaco, campos particulares
- Variação de tipo : tipos exatos, tipo DeepReadonly
- Exceções : qualquer mônada
- invalidação de refinamento : funções puras
- operações inseguras (acesso ao índice) : mônadas / talvez

dados imutáveis, funções puras, mônadas ... Parabéns , provamos que FP é legal!

achados


  • TS quer encontrar um equilíbrio entre correção e produtividade
  • Os testes podem provar segurança e correção apenas para dados de teste.
  • Os tipos podem provar a segurança geral de um programa.
  • Mutação - ruim, ok?

Como a DZ recomendaria jogar com o Flow corrigindo um erro simples:

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

Código de exemplo, solução e análise do problema, links úteis no repositório .

All Articles