打字稿:不当行为或放纵可靠性

目的是显示TS在哪里给人以安全感,让您在程序运行时出错。

我们不会谈论错误,在TS中有
1,500个已打开的错误和6,000个已关闭的错误(“是:问题是:打开标签:错误”),

所有示例都将考虑:

  • TS 严格模式已启用(理解时写了一篇文章)
  • 没有显式的“ any”:“ as any”,“ Objects”,“ Function”,{[key:string]:未知}
  • 没有隐式的“ any”:(noImplicitAny):无类型的导入(纯JS文件),错误的类型推断
  • 无需对类型进行错误的猜测:来自服务器的响应,第三方库的键入

内容:

  • 介绍
  • 标称类型,自定义类型-当事物看起来相同但相异时
  • 类型差异,确切类型-关于类型之间的关系
  • 优化无效-谈论信任
  • 例外-搞砸了值得承认吗?
  • 不安全的操作-信心并不总是很好
  • 奖励案例-在PR审查阶段进行类型检查
  • 结论

介绍


在JS中编写将两个数字相加的函数难吗?天真的实现

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

让我们检查一下sum(2,2)=== 4`的实现,一切似乎都正常吗?并非如此,当我们描述一个函数时,我们应该考虑各种输入值以及该函数可以返回什么

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'

健全性是分析仪证明程序运行时没有错误的能力。如果分析仪接受了该程序,则可以保证它是安全的。

安全程序是可以永久运行而不会出错的程序。那些。该程序不会崩溃或引发错误。

正确的程序-可以执行和不应该执行的程序。正确性取决于业务逻辑的执行。

类型可以证明该程序整体上是安全的,并且仅在测试数据内测试该程序是安全且正确的(100%覆盖率,stryker缺少“突变体”,通过基于属性的测试等均不能证明任何内容,并获得许可)降低风险)。有传说说定理证明可以证明程序的正确性。

重要的是要了解TS的原理,了解该工具正在尝试解决的问题,重要的是在未解决的问题上。

关于Soundness
TS 的注释跳过了一些在编译阶段不确定的操作。仔细考虑有不良行为的地方。

设计目标
不是TS的目标-建立具有安全性保证的类型系统,而是着重于安全性和生产率之间的平衡

示例结构:
问题不是安全行为,列表可能不完整,这是我在文章,报告, TS git问题。
该建议是3-4年前公开发行的TS问题,作者发表了大量评论和有趣的解释
提示- 作者的恕我直言,作者认为良好做法

结构型与名义型


结构型与名义型1.问题


结构化类型 -比较类型时不考虑其名称或声明位置,而是根据“结构”比较类型。

我们想将字母“ sendEmail”发送到正确的地址“ ValidatedEmail”,有一个检查地址“ validateEmail”的函数,该函数返回正确的地址“ ValidatedEmail”。不幸的是,TS允许您将任何字符串发送到`sendEmail`,因为 TS的“ ValidatedEmail”与“ 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");

结构型与名义型1.报价


github.com/microsoft/TypeScript/issues/202
输入关键字“ nominal”,以便名义上检查类型。现在我们可以禁止仅在预期为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');

结构型与名义型1.提示


我们可以创建一个“ Opaque”类型,该类型将采用一些“ T”并通过与通过传递的“ K”创建的类型相结合而赋予其唯一性。K可以是唯一符号(唯一符号),也可以是字符串(然后必须确保这些字符串是唯一的)。

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

结构型与名义型2.问题


我们有一个Dollar和Euro类,每个类都有一个add方法,用于将Dollar添加到Dollar,将Euro添加到Euro。对于TS,这些类别在结构上是相等的,我们可以将美元加到欧元上。

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

结构型与名义型2.报价


github.com/microsoft/TypeScript/issues/202
句子都是一样的,带有`nominal`,但是因为 由于类可以神奇地变为标称的(稍后会详细介绍),因此考虑了以更明确的方式进行这种转换的可能性。

结构型与名义型1.提示


如果该类具有私有字段(以“#”或TS c“ private”为本地),则该类将神奇地变为Nominal,名称和值可以是任何值。“!”(确定分配断言)用于防止TS在未初始化的字段上发誓(strictNullChecks,strictPropertyInitialization标志已启用)。

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

类型差异1.问题


简而言之,编程选项是能够在其中传递Type的Supertype / Subtype的能力。例如,有一个层次结构Shape-> Circle-> Rectangle,如果需要Circle,是否可以传输或返回Shape / Rectangle?

变体编程HABRSO

我们可以将带有数字所在字段的类型传递给一个函数,该函数期望该字段为字符串或数字,并使体内的传输对象发生变化,从而将该字段更改为字符串。那些。`{status:number}为{status:number | string {}:{技巧:将数字转换为字符串,从而导致意外错误。

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

类型差异1.报价


github.com/Microsoft/TypeScript/issues/10717
建议引入“ in / out”以明确限制泛型的协方差/逆方差。

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

类型差异1.提示


如果我们使用不可变的结构,则不会出现此类错误(我们已经启用了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());

类型差异1.奖金


Readonly可分配给可变的
github.com/Microsoft/TypeScript/issues/13347
github.com/microsoft/TypeScript/pull/6532#issuecomment-174356151

但是,即使我们创建了Readonly类型,TS也不会禁止传递给其中的函数只读`Readonly <{readonly status:number}> as {状态:number | 字符串}为{状态:字符串}`

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

类型差异2。问题


对象可能包含其他字段,这些字段所对应的类型没有这些字段:`{message:string; 状态:字符串}为{消息:字符串}`。因此,某些操作可能不安全

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认为由于合并的结果,状态{{... {message:string,status:number},... {message:string}}`status将是一个数字。

实际上,“ {... {消息:“未找到”,状态:404},... {消息:“无数据”,状态:“ NotFound”},}`状态-字符串。

类型差异2.报价


github.com/microsoft/TypeScript/issues/12936
引入“精确”类型或类似的语法来说明类型不能包含其他字段。

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

类型差异2.提示


通过显式列出字段或过滤出未知字段来合并对象。

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

细化无效。问题


在证明了有关外部状态的某些信息之后,调用函数并不安全,因为 无法保证函数不会更改此外部状态:

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

细化无效。句子


github.com/microsoft/TypeScript/issues/7770#issuecomment-334919251
为函数添加`pure`修饰符,这至少将允许您信任此类函数

细化无效。小费


使用不可变数据结构,则该函数调用对于先前的检查将是先验安全的。

奖金


流类型是如此之强,以至于它没有上面列出的所有问题,但是运行得如此之快,以至于我不建议您使用它。

例外情况。问题


TS不能以任何方式帮助处理Exception;在函数签名上没有任何清楚的地方。

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

例外情况。句子


github.com/microsoft/TypeScript/issues/13219
建议引入一种语法,该语法允许在函数签名中显式描述异常

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

例外情况。奖金


github.com/microsoft/TypeScript/issues/6283
由于某种原因,在TS中,Promise的类型定义会忽略错误的类型

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

例外情况。小费


以Promise之类的Either容器为例,只进行最佳键入。任一实现示例

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

不安全的操作。问题


如果我们有一个固定大小的元组,则TS可以保证所请求的索引上有某些内容。这不适用于阵列,TS将信任我们

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

不安全的操作。句子


github.com/microsoft/TypeScript/issues/13778
建议将undefined添加到返回类型T中,以访问数组。但是在这种情况下,当访问任何索引时,您将必须使用`?`或进行显式检查。

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

不安全的操作。小费


为了不产生多余的实体,我们使用先前已知的容器“ Either”并编写一个用于处理索引的安全函数,该函数将返回“ Either <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()));

奖金 重新加载功能


如果我们要说一个函数采用几行并返回一个字符串或采用几个数字并返回一个数字,则在实现中,这些签名将是连续的,程序员应保证其正确性,但必须在TS上。

PS通过泛型和条件类型查看替代方案:

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


奖金 防护罩


TS相信程序员“ isSuperUser”可以正确确定谁是“ SuperUser”,并且如果添加了“ Vasya”,则不会出现提示。

PS值得思考的是,在联合的阶段我们将如何区分类型-带标签的联合

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

提示结论


-标称类型:不透明类型,私有字段
-类型差异:确切类型,DeepReadonly类型
-异常:要么monad-
细化无效:纯函数
-不安全的操作(索引访问):要么/也许是monads

不可变的数据,纯函数,monads ...恭喜,我们证明FP很酷!

发现


  • TS希望在正确性和生产率之间取得平衡
  • 测试只能证明测试数据的安全性和正确性。
  • 类型可以证明程序的整体安全性。
  • 变异-不好,好吗?

正如DZ建议使用Flow纠正了一个简单的错误:

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

示例代码,问题的解决方案和分析,以及存储库中的有用链接

All Articles