Naskah: perilaku tidak sehat atau indulgensi keandalan

Tujuannya adalah untuk menunjukkan di mana TS memberikan ilusi keamanan, yang memungkinkan Anda untuk mendapatkan kesalahan saat program sedang berjalan.

Kami tidak akan membicarakan bug, di TS ada cukup
1.500 bug terbuka dan 6.000 ditutup ('is: masalah adalah: label terbuka: Bug').

Semua contoh akan dipertimbangkan dengan:

  • Mode ketat TS aktif (menulis artikel sambil memahami)
  • Tanpa "any": "as any", "Objects", "Function", {[key: string]: tidak diketahui}
  • Tanpa "apa pun" tersirat: (noImplicitAny): impor yang tidak diketik (file JS murni), inferensi jenis yang salah
  • Tanpa tebakan salah tentang jenis: respons dari server, mengetik pustaka pihak ketiga

Kandungan:

  • pengantar
  • Tipe nominal, tipe khusus - ketika semuanya tampak sama, tetapi sangat berbeda
  • Ketik varians, tipe tepat - tentang hubungan antar jenis
  • Pembatalan penyempurnaan - bicara tentang kepercayaan
  • Pengecualian - apakah layak untuk mengakuinya ketika dikacaukan?
  • Operasi yang tidak aman - kepercayaan diri tidak selalu baik
  • Kasing Bonus - Pengecekan Jenis pada Tahap Tinjauan PR
  • Kesimpulan

pengantar


Apakah sulit untuk menulis fungsi untuk menambahkan dua angka dalam JS? Ambil implementasi yang naif

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

Mari kita periksa implementasi `sum (2, 2) === 4` kami, apakah semuanya berjalan dengan baik? Tidak juga, ketika kita mendeskripsikan suatu fungsi, kita harus memikirkan semua jenis nilai input, serta fungsi yang dapat dikembalikan

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'

Tingkat kesehatan adalah kemampuan penganalisa untuk membuktikan bahwa tidak ada kesalahan saat program sedang berjalan. Jika program diterima oleh penganalisa, maka dijamin aman.

Program aman adalah program yang dapat bekerja selamanya tanpa kesalahan. Itu program tidak akan crash atau melempar kesalahan.

Program yang benar - program yang melakukan apa yang seharusnya dan tidak melakukan apa yang seharusnya tidak. Kebenaran tergantung pada pelaksanaan logika bisnis.

Jenis dapat membuktikan bahwa program secara keseluruhan aman, dan menguji bahwa program tersebut aman dan benar hanya dalam data uji (cakupan 100%, tidak adanya "mutan" dari stryker, lulus tes berbasis properti dan sebagainya tidak dapat membuktikan apa pun, dan lisensi mengurangi risiko). Ada legenda yang membuktikan teorema dapat membuktikan kebenaran program.

Penting untuk memahami filosofi TS, untuk memahami apa yang coba dipecahkan oleh alat dan, yang penting, apa yang tidak coba dipecahkan.

Catatan tentang Soundness
TS melompati beberapa operasi yang tidak yakin pada tahap kompilasi. Tempat-tempat dengan perilaku tidak sehat dipikirkan dengan cermat.

Tujuan desain
Bukan tujuan untuk TS - Untuk membuat sistem tipe dengan jaminan keamanan, alih-alih fokus pada keseimbangan antara keselamatan dan produktivitas

Contoh struktur:
Masalahnya adalah perilaku yang tidak aman, daftar mungkin tidak lengkap, ini yang saya temukan di artikel, laporan, Masalah TS git.
Proposal adalah masalah TS terbuka 3-4 tahun yang lalu, dengan banyak komentar dan penjelasan menarik oleh penulis
.Tip - IMHO penulis, apa yang penulis anggap praktik yang baik

Pengetikan struktural vs Nominal


Pengetikan Struktural vs Nominal 1. Masalah


Mengetik struktural - ketika membandingkan jenis tidak memperhitungkan nama mereka atau di mana mereka dinyatakan, dan jenis dibandingkan menurut "struktur".

Kami ingin mengirim surat `sendEmail` ke alamat yang benar` ValidatedEmail`, ada fungsi untuk memeriksa alamat` validateEmail` yang mengembalikan alamat yang benar` ValidatedEmail`. Sayangnya TS memungkinkan Anda untuk mengirim string apa pun ke `sendEmail`, karena `ValidatedEmail` untuk TS tidak berbeda dengan` 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");

Pengetikan Struktural vs Nominal 1. Penawaran


github.com/microsoft/TypeScript/issues/202
Masukkan kata kunci `nominal` sehingga jenisnya diperiksa secara nominal. Sekarang kita bisa melarang lewat `string` di mana` ValidatedEmail` diharapkan

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

Pengetikan Struktural vs Nominal 1. Tip


Kita dapat membuat tipe `Buram`, yang akan mengambil sebagian` T` dan memberinya keunikan dengan menggabungkannya dengan tipe yang dibuat dari `K` yang dilewati. `K` dapat berupa simbol unik (` simbol unik`) atau string (maka akan diperlukan untuk memastikan bahwa string ini unik).

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

Pengetikan Struktural vs Nominal 2. Masalah


Kami memiliki kelas Dolar dan Euro, masing-masing kelas memiliki metode menambahkan untuk menambahkan Dolar ke Dolar dan Euro ke Euro. Untuk TS, kelas-kelas ini secara struktural sama dan kita dapat menambahkan Dolar ke 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);

Pengetikan Struktural vs. Nominal 2. Penawaran


github.com/microsoft/TypeScript/issues/202
Kalimatnya sama, dengan `nominal`, tetapi karena Karena kelas secara ajaib dapat menjadi Nominal (lebih lanjut tentang itu nanti), kemungkinan membuat transformasi seperti itu dengan cara yang lebih eksplisit dipertimbangkan.

Pengetikan Struktural vs Nominal 1. Tip


Jika kelas memiliki bidang pribadi (asli dengan `#` atau dari TS c `private`), maka kelas secara ajaib menjadi Nominal, nama dan nilai dapat berupa apa saja. `!` (Penentuan penugasan pasti) digunakan untuk mencegah TS bersumpah pada bidang yang tidak diinisialisasi (strictNullChecks, flag strictPropertyInitialization diaktifkan).

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

Ketik varians 1. Masalah


Opsi pemrograman, singkatnya, adalah kemampuan untuk melewati Supertype / Subtype di sana, di mana Type diharapkan. Misalnya, ada hirarki Shape -> Circle -> Rectangle, apakah mungkin untuk mentransfer atau mengembalikan Shape / Rectangle jika Circle diharapkan?

Varian dalam pemrograman habr , SO .

Kita bisa meneruskan tipe dengan bidang di mana angka terletak pada fungsi yang mengharapkan bidang sebagai string atau angka, dan memutasi objek yang ditransmisikan dalam tubuh, mengubah bidang menjadi string. Itu `{status: number} sebagai {status: number | string} as {status: string} `di sini ada trik seperti mengubah angka menjadi string, menyebabkan kesalahan kejutan .

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

Ketik varians 1. Penawaran


github.com/Microsoft/TypeScript/issues/10717
Disarankan untuk memperkenalkan `masuk / keluar` untuk secara eksplisit membatasi kovarians / contravariance untuk obat generik.

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

Ketik varians 1. Tip


Jika kami bekerja dengan struktur yang tidak dapat diubah, maka tidak akan ada kesalahan seperti itu (kami telah mengaktifkan flag 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());

Ketik varians 1. Bonus


Readonly dapat dialihkan ke
github.com/Microsoft/TypeScript/issues/13347
github.com/microsoft/TypeScript/pull/6532#issuecomment-174356151

, tetapi jika kita membuat tipe Readonly, TS tidak akan melarang untuk beralih ke fungsi di mana tidak diharapkan Readonly `Readonly <{status readonly: number}> sebagai {status: number | string} sebagai {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());

Ketik varians 2. Masalah


Objek dapat berisi bidang tambahan yang jenisnya tidak memiliki: `{message: string; status: string} as {message: string} `. Karena itu beberapa operasi mungkin tidak aman

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 berpikir bahwa sebagai hasil dari penggabungan, status `{... {message: string, status: number}, ... {message: string}}` akan berupa angka.

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

Ketik varians 2. Penawaran


github.com/microsoft/TypeScript/issues/12936
Memperkenalkan tipe `Exact` atau sintaksis serupa untuk mengatakan bahwa suatu tipe tidak dapat berisi bidang tambahan.

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

Ketik varians 2. Tip


Gabungkan objek dengan mendaftarkan bidang secara eksplisit atau memfilter bidang yang tidak dikenal.

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

Penyempurnaan penyempurnaan. Masalah


Setelah kami membuktikan sesuatu tentang keadaan eksternal, fungsi panggilan tidak aman, karena Tidak ada jaminan bahwa fungsi tidak mengubah keadaan eksternal ini:

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

Penyempurnaan penyempurnaan. Kalimat


github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251
Tambahkan pengubah `murni` untuk fungsi, ini setidaknya akan memungkinkan Anda untuk mempercayai fungsi tersebut

Penyempurnaan penyempurnaan. Tip


Gunakan struktur data yang tidak berubah, maka pemanggilan fungsi akan menjadi apriori aman untuk pemeriksaan sebelumnya.

Bonus


Jenis aliran sangat kuat sehingga tidak memiliki semua masalah yang tercantum di atas, tetapi sangat berjalan sehingga saya tidak akan merekomendasikan menggunakannya.

Pengecualian. Masalah


TS tidak membantu untuk bekerja dengan Pengecualian dengan cara apa pun, tidak ada yang jelas pada tanda tangan fungsi.

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

Pengecualian. Kalimat


github.com/microsoft/TypeScript/issues/13219
Disarankan untuk memperkenalkan sintaksis yang memungkinkan secara eksplisit menjelaskan Pengecualian dalam tanda tangan fungsi

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

Pengecualian. Bonus


github.com/microsoft/TypeScript/issues/6283
Untuk beberapa alasan, dalam TS, definisi tipe untuk Janji mengabaikan jenis kesalahan

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

Pengecualian. Tip


Ambil salah satu wadah seperti Janji, dengan hanya mengetik yang terbaik. ( Entah contoh implementasi )

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

Operasi tidak aman. Masalah


Jika kami memiliki tupel dengan ukuran tetap, maka TS dapat menjamin bahwa ada sesuatu pada indeks yang diminta. Ini tidak akan berfungsi untuk array dan TS akan mempercayai kami

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

Operasi tidak aman. Kalimat


github.com/microsoft/TypeScript/issues/13778
Disarankan untuk menambahkan `tidak terdefinisi` ke tipe pengembalian` T` untuk akses indeks ke array. Tetapi dalam kasus ini, ketika mengakses pada indeks apa pun, Anda harus menggunakan `?` Atau melakukan pemeriksaan eksplisit.

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

Operasi tidak aman. Tip


Agar tidak menghasilkan entitas di luar kebutuhan, kami mengambil wadah yang sebelumnya dikenal `Either` dan menulis fungsi aman untuk bekerja dengan indeks, yang akan kembali` Entah <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()));

Bonus Reload fungsi


Jika kita ingin mengatakan bahwa suatu fungsi mengambil beberapa baris dan mengembalikan string atau mengambil beberapa angka dan mengembalikan angka, maka dalam implementasi tanda tangan ini akan berdekatan, dan programmer harus menjamin kebenarannya, tetapi pada TS.

PS melihat alternatif melalui tipe generik dan kondisional:

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 Ketik penjaga


TS mempercayai programmer bahwa `isSuperUser` dengan benar menentukan siapa` SuperUser` dan jika `Vasya` ditambahkan, tidak akan ada konfirmasi.

PS layak untuk dipikirkan tentang bagaimana kita akan membedakan tipe yang sudah pada tahap union-tagged union mereka

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

Kesimpulannya ada pada tipsnya


- Jenis nominal : Jenis buram, bidang pribadi
- Jenis varians : Jenis yang tepat, Jenis DeepReadonly
- Pengecualian : Salah satu monad
- Penyangkalan penyempurnaan : Fungsi murni
- Operasi tidak aman (akses indeks) : Either / Mungkin monad

Data tidak dapat diubah, fungsi murni, mon ... Selamat , kami membuktikan bahwa FP itu keren!

temuan


  • TS ingin mencapai keseimbangan antara kebenaran dan produktivitas
  • Tes dapat membuktikan keamanan dan kebenaran hanya untuk data uji.
  • Jenis dapat membuktikan keamanan keseluruhan program.
  • Mutasi - buruk, oke?

Seperti DZ akan merekomendasikan untuk bermain dengan Flow setelah memperbaiki kesalahan sederhana:

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

Contoh kode, solusi dan analisis masalah, tautan yang bermanfaat di repositori .

All Articles