[TypeScript-tsconfig-noUncheckedIndexedAccess-01](TS 4.0 之后影响比较大的一个编译器配置选项)

603 阅读6分钟

不知道这个配置选项能写几篇,但是这个配置选项的复杂是出乎我意料的。我会持续把我能搜集到的 stackoverflow 以及 TypeScript 的 issue 的文章都搜集和测试写下来。

起因:因为我刚搞完一个版本,正在开心的准备 build 的时候,灵机一动,看看 有啥新的 tsconfig 没加的选项填上,然后就加到了这个配置选项,加上以后,成片报错,我就知道,这个配置选项了不得。。。

官方的介绍-noUncheckedIndexedAccess

TypeScript 通过 index signatures 来描述一个 object 有 unknown 的 keys,但是 known 的 values。

// 以下为 noUncheckedIndexedAccess 为关闭
interface EnvironmentVars {
  NAME: string;
  OS: string;
 
  // Unknown properties are covered by this index signature.
  [propName: string]: string;
}
 
declare const env: EnvironmentVars;
 
// Declared as existing
const sysName = env.NAME;
const os = env.OS;
      
// const os: string
 
// Not declared, but because of the index
// signature, then it is considered a string
const nodeEnv = env.NODE_ENV;
        
// const nodeEnv: string

打开 noUncheckedIndexedAccess,会给没有声明的字段添加 undefined 的标签。

declare const env: EnvironmentVars;
 
// Declared as existing
const sysName = env.NAME;
const os = env.OS;
      
// const os: string
 
// Not declared, but because of the index
// signature, then it is considered a string
const nodeEnv = env.NODE_ENV;
        
// const nodeEnv: string | undefined

从这个例子也能看出来,这个配置选项加了以后,凡是没有在接口里声明的对象的属性,会添加一个 undefined 类型。

这个类型的意义在于,因为没有对特定的 key 进行限制,所以有可能这个对象里,没有这个 key 对应的 value。这个情况确实常见,因为常见接口是确定的,确定的都可以写好,一般这种不确定的 key 的,可能存的是从 程序外部扫上来,或者传过来的东西,确实有可能是没有的。

目前遇到的问题:

webstorm 支持问题

似乎对这个配置选项支持是有问题的。我在 webstorm 的 youtrack 看到挺多对于这个 4.1 引入的特性支持不好的 issue。表现是,build 通不过,报错了。但是文件里显示还没加上 undefined。

我提了 issue。但是神奇的是,我用 vscode 搞了会,commit 提交以后。webstorm 居然好了。我不知道是我的哪个操作又触发了这个东西。

这个配置带来了新的 type 不一致。

issue 1

github.com/microsoft/T…

type A = string[];
​
/** These are type errors but shouldn't be, as `A[number]` should include `undefined` */
// 当对 array type 进行 indexed access 时(比如 (number[])[number])),结果应该包括 undefined,但是没有包括。
// 这个让 noUncheckedIndexedAccess 提供的类型安全 不安全了。
// 对于 Record 也有这个问题。
const x: A[number] = undefined;
const y: A[0] = undefined;
const z: A[1000] = undefined;
​
​
// 下面这段代码,这个配置选项开和不开,是不一样的。
// 在打开这个选项时,get 有 undefined ,而 get2 没有。
// 自然,关闭这个,都没有。
​
​
/** Without return type annotation */
const get = <O, K extends keyof O>(o: O, k: K) => o[k];
​
/** With return type annotation */
// 这个和上面的原理是一样的,因为直接取 type 是没有 undefined
const get2 = <O, K extends keyof O>(o: O, k: K): O[K] => o[k];
​
/** A type error as expected */
const a: number = get([1], 1);
​
/** No type error, because `get2` happens to include a return type annotation */
const b: number = get2([1], 1);
​
// issue 期望,当对 array type 进行 indexed access 时(比如 (number[])[number])),结果应该包括 undefined
​
​
楼主给出的解决方案:
type Get<O, K extends keyof O> =
  O extends any[]
  ? number extends O['length'] // If `number` is a subtype of `O.length` then `O` is _not_ a tuple, since a tuple has a specific length
    ? O[K] | undefined
    : O[K]
  : number extends keyof O // If O is not an array and `number` is a subtype of `keyof O` then `O` is a `Record<number, any>`
  ? O[K] | undefined
  : string extends keyof O // If `string` is a subtype of `keyof O` then `O` is a `Record<string, any>`
  ? O[K] | undefined
  : O[K];

issue 2

github.com/microsoft/T…

declare const foo: number[]
​
// 这个表达式的意思是,获取 foo[1] 的 type 但是没有对 foo[1] 进行 access 操作。
type Foo = typeof foo[1] //number// 这里对 foo[1] 进行了 access,因为没有显示指定 1 的类型,所以添加了 undefined。
const bar: Foo = foo[1] //Type 'number | undefined' is not assignable to type 'number'typeof on object index access types don't include undefined when noUncheckedIndexedAccess · Issue #42471 · microsoft/TypeScriptdeclare const foo: number[]// 这个表达式的意思是,获取 foo[1] 的 type 但是没有对 foo[1] 进行 access 操作。
type Foo = typeof foo[1] //number// 这里对 foo[1] 进行了 access,因为没有显示指定 1 的类型,所以添加了 undefined。
const bar: Foo = foo[1] //Type 'number | undefined' is not assignable to type 'number'

noUncheckedIndexedAccess 不改变原有的类型,只改变获取类型时,添加一个安全检查。

// 注意以下都是写,没有读。
const arr1: number[] = [];
arr1.push(undefined); // Forbidden, array does not store undefined values.const arr2: (number | undefined)[] = [];
arr2.push(undefined); // Allowed, array does store undefined values.// 注意以下是读,即 access
const arr = [undefined];
console.log(arr.hasOwnProperty(0)); // true, element at index 0 exists (even if the value is undefined).
console.log(arr.hasOwnProperty(1)); // false, no element at index 1 exists.

总结

结合 这两个 issue 其实可以看到。noUncheckedIndexedAccess 的一些特性:

  • 无论是否打开,不影响显示的类型计算(但是会影响 access 时的隐式类型计算,这个是个坑,之前的代码无论是类型还是非类型,在打开了这个以后,都会出一些问题)
  • 影响 access 时的类型,简单说,如果 interface 没有指明,或者一个数组没有指明,自动加一个 undefined。

额外

我还在其他一些 issue 看到一些和这个有关的讨论

  • 有一个提出,TS 确实 missing 这样语义的类型。
  • noUncheckedIndexedAccess 实际上对于一个成员的获取和赋值看成了两个状态。所以有 issue 期望增加类型系统的能力,去区分一个成员的获取和赋值的过程

接下来的工作

翻译 --noUncheckedIndexedAccess by RyanCavanaugh · Pull Request #39560 · microsoft/TypeScript

这个是 noUncheckedIndexedAccess 配置项的 PR。通过看这个 issue 也才知道。tsconfig 的配置如果没有 strick,是官方认为还不那么高效和优雅的选项。目前这个选项显然符合这一点。实际上这个选项对于目前 ts 能支持的粒度提出了挑战。

就像上面两个 issue,开发者认为,如果你影响了类型,我就应该能获取这个类型,这个想法不能说有问题。但是因为这个选项是在不改变原有实际类型系统的情况下,改变了实际类型检查中的类型,出现了一定的不一致。

而且,通过看这个 issue ,可以看到,这个问题,确实还真是的问题。尤其是框架代码中,因为框架中肯定有数据是临时注册进来的,框架并不知道用户写了啥类,但是类肯定有元数据,这些元数据肯定要存在一个对象,或者数组中。

现在其实是,自己就认为,肯定没问题。但是实际上,并没有保证这个事儿。而这一块代码,其实是所有框架的核心部分。因为无论一个框架支持了啥,你都有一大堆对象在一个 list 里等着被读出来。而你不可能预先定义好接口,只能是一个模糊的定义。

按照官方的说法,目前这个选项似乎算是测试性质,如果他们认为 ok 了会把这个改为 strickxxxx 这样的选项。

其实这个选项对于业务代码影响有限,可以考虑不开。但是如果开了的话,以前的代码影响会比较大。

对于新项目,建议开。感受一下这个问题。按照目前的讨论来看,ts 增加对于一个操作读和写两套类型,应该不远了。另外,这个 missing 关键字,也说不定会有。这个我后面也搜集一些相关内容。