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 existem1.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ênuafunction 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 retornar1.1 + 2.7
NaN + 2
99999999999999992 + 99999999999999992
2n + 2
{} + true
2 + '2'
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 SoundnessTS 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 projetoNã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 deexemplo: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áticasTipagem 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/202Digite 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>;
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);
dollars100.add(dollars100);
euro100.add(euro100);
dollars100.add(euro100);
Tipagem estrutural vs nominal 2. Oferta
github.com/microsoft/TypeScript/issues/202A 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 {
private desc!: never;
value: number;
constructor(value: number) {
this.value = value;
}
add(dollar: Dollar) {
return new Dollar(dollar.value + this.value);
}
}
class Euro {
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);
dollars100.add(dollars100);
euro100.add(euro100);
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);
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 };
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 }>) {
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 mutablegithub.com/Microsoft/TypeScript/issues/13347github.com/microsoft/TypeScript/pull/6532#issuecomment-174356151Mas, 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);
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 segurasconst 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 };
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/12936Introduzindo 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, };
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 };
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) {
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);
logAge(person.name, person.age);
}
Invalidação de refinamento. Frase
github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251Adicione o modificador `pure` para funções, isso permitirá pelo menos confiar nessas funçõesInvalidaçã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çãoimport { 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 {
return error as never;
}
}
}
console.log(getJokeSafe(true));
Exceções. Bônus
github.com/microsoft/TypeScript/issues/6283Por algum motivo, no TS, a definição de tipo para Promise ignora o tipo de erroconst promise1: Promise<number> = Promise.resolve(42);
const promise: Promise<never> = Promise.reject(new TypeError());
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)
.mapLeft(error => {
if (error instanceof JokeError) {
console.log("JokeError");
} else {
exhaustiveCheck(error);
}
})
.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
export const constNumbers: readonly [1, 2, 3]
= [1, 2, 3] as const;
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/13778Propõ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.
const dynamicNumbers: number[] = [1, 2, 3];
console.log(dynamicNumbers[100].toFixed());
console.log(dynamicNumbers[100]?.toFixed());
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);
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 taggedtype 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) {
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 / talvezdados 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:
declare function log(arg: { name: string, surname?: string }): void;
const person: { name: string } = { name: 'Negasi' };
log(person);
Código de exemplo, solução e análise do problema, links úteis no repositório .