TypeScript (几乎)任意函数都可以分配给一个Assert函数类型的变量,

jecbmhm3  于 10个月前  发布在  TypeScript
关注(0)|答案(4)|浏览(87)

Bug报告

Bug报告

🔎 搜索词

Assert函数、可赋值性、不一致的类型保护

🕗 版本与回归信息

  • 这种行为在每个我尝试过的版本中都存在,我也查阅了关于类型系统行为的常见问题解答条目

⏯ Playground链接

包含相关代码的Playground链接

💻 代码

  1. const assertNumber = (x: unknown): asserts x is number => {
  2. if (typeof x !== 'number') {
  3. throw Error("not a number")
  4. }
  5. }
  6. // suspicious assignment...
  7. const assertString: (x: unknown) => asserts x is string = assertNumber
  8. const x: unknown = 100;
  9. assertString(x); // Because `assertString` is actually `assertNumber`, no error is thrown
  10. /*** x's type is now string ***/
  11. x.substring(1); // no compile error, but runtime error!
  12. const foo = (x: string | undefined): number => 100
  13. // suspicious assignment...
  14. const assertTruthy: (x: string | undefined) => asserts x = foo
  15. const y: string | undefined = false ? 'a' : undefined;
  16. assertTruthy(y);
  17. y.substring(1); // no compile error, but runtime error!

🙁 实际行为

(可疑的)两个赋值操作都成功执行,没有编译错误。这种行为与类型保护的行为不一致。

🙂 预期行为

这两个赋值操作都应该被编译器拒绝。
我认为Assert函数的可赋值性规则应该与类型保护的规则一致。至少,一个没有Assert函数签名的函数被赋值给一个Assert函数类型的变量应该被拒绝。此外,对于具有"结果类型"( (x: ...) => asserts x is T )的Assert函数签名,它应该与"结果类型"保持协变性。

jbose2ul

jbose2ul1#

至少,将一个没有Assert函数签名的函数分配给一个Assert函数类型的变量应该被拒绝。
原始笔记在#32695中没有提到这一点,但据我所知,这种逻辑的动机是我们希望与Assert函数类型绑定的绑定可以通过“普通”函数进行初始化,即:

  1. const assertNumber: (x: unknown) => asserts x is number = x => {
  2. if (typeof x !== 'number') {
  3. throw Error("not a number")
  4. }
  5. }

在这里,函数表达式的返回类型仅仅是void,因此根据这个规则,初始化是非法的,除非我们添加了新的逻辑来例如上下文地修改函数表达式的返回类型。
将具有两种不同Assert类型的东西分配给是非常草率的。我们可能会标记这个问题,但我认为过去三年的生活经验是,任何类型的Assert函数的别名非常罕见,因为它们没有任何有意义的替代或多态性。

k5hmc34c

k5hmc34c2#

别名Assert函数...没有任何有意义的替代或多态性。
我不同意,并认为这是一个合法的正确性问题!考虑以下内容,其中包括定义泛型数组Assert函数的一些其他内容:

  1. type AssertFn<T> = (val: unknown) => asserts val is T;
  2. const num: AssertFn<number> = (val) => {
  3. if (!Number.isFinite(val)) {
  4. throw new Error('num assertion failed');
  5. }
  6. }
  7. const array: <T>(assert: AssertFn<T>) => AssertFn<T[]> =
  8. (assert) => (val) => {
  9. if (!Array.isArray(val)) {
  10. throw new Error('assertIsArray failed');
  11. }
  12. val.forEach(assert);
  13. };
  14. const assertStringArray: AssertFn<string[]> = array(num);

链接到Playground
请注意,我明确地将 assertStringArray 标记为 AssertFn<string[]> ,但实际上通过 array(num) 提供了一个 AssertFn<number[]> ,编译器对此没有问题。如果我删除了显式类型注解,它会正确地推断出类型为 AssertFn<number>

展开查看全部
iqih9akk

iqih9akk3#

一个可能的原因是,它要求指定Assert类型以确保它们影响CFA。

  1. type TypePredicate<T> = (v: unknown) => v is T;
  2. type TypeAssertion<T> = (v: unknown) => asserts v is T;
  3. declare function makeAssertion<T>(predicate: TypePredicate<T>): TypeAssertion<T>;
  4. declare function isString(v: unknown): v is string;
  1. const assertIsString = makeAssertion(isString);
  2. declare let x: unknown;
  3. assertIsString(x); // TS error! Assertions require every name in the call target to be declared with an explicit type annotation
  1. - const assertIsString = makeAssertion(isString);
  2. + const assertIsString: TypeAssertion<string> = makeAssertion(isString);
  3. declare let x: unknown;
  4. assertIsString(x);
  5. x; // good, x is string

但是如果我们放入错误的类型...

  1. - const assertIsString = makeAssertion(isString);
  2. + const assertIsString: TypeAssertion<number> = makeAssertion(isString);
  3. declare let x: unknown;
  4. assertIsString(x);
  5. x; // bad, x is number

我本以为 const assertIsString: TypeAssertion<number> = makeAssertion(isString); 会产生类型错误。
还有一个例子:

  1. type TypeAssertion<T> = (v: unknown) => asserts v is T;
  2. declare function sendRequest<ExpectedReponse>(
  3. path: string,
  4. assertion: TypeAssertion<ExpectedReponse>
  5. ): Promise<ExpectedReponse>;
  6. interface Person { name: string };
  7. type People = Array<Person>;
  8. declare function assertPerson(v: unknown): asserts v is Person;
  9. // @ts-expect-error
  10. sendRequest<People>("/db", assertPerson);

playground

展开查看全部
guykilcj

guykilcj4#

这种行为对我来说非常令人惊讶。我一直在做一个项目,该项目使用Assert函数验证传递给API端点的JSON数据。我震惊地意识到代码实际上没有类型安全。项目中的其他人也没有意识到这一点,包括编写原始代码的人。有关问题的非常简化的代码版本如下:

$x_1a^0b^1^x$

相关问题