类型防护将泛型扩展到所有可能的值?

时间:2019-08-29 17:42:26

标签: typescript generics

这是一个简化的代码片段,可以重现我的问题:

type Supported = 'foo'|'bar';
type State = {
  [K in Supported]?: K[]
}
function test<T extends keyof State>(state: State, type: T) {
  const arr = state[type];
  if (!arr) {
    return;
  }
  return arr[0];
}
function test2<T extends keyof State>(state: State, type: T) {
  const arr = state[type] as T[]|undefined;
  if (!arr) {
    return;
  } 
  return arr[0];
}

在第一个函数中,arr的类型是if块之前的State [T]。变成“ foo” [] |之后是“ bar” []。 在test2中,我将arr手动转换为状态的实际值类型,并且返回值类型正确。

在类型保护过滤出未定义的值之后,通用类型T似乎丢失了。这是预期的行为吗?

http://www.typescriptlang.org/play/#code/C4TwDgpgBAygrmMB7ATsCATKBeKByAMySTwB88AjAQxTwG4AoUSWYK9HKAbwaigG0A0lACWAO1gJkaTAF0A-AC4og-rIYBfBgThiAxsBFIJ6AM7AAPABUoEAB7oxGU1ADWEEEgKt2EAHwAFOa+yjBs6AA0UMwQylYAlNy8UHrG5lA0KJzB6Pwxsox8It4BAISZiTx8fCgQwHAoYoVQWjV1DRKZ-AAMBVAA9P0pacAZKCjKAERESJNqUKRQk9Qoc+paOvqGxtEQ5gBM1rYOEE4u7p7eYb6BObE+kdHg9wlJfKli6ZnZ4RB5z7IMi4rGpSLoMBACOJMM1ilAyhU3tVavVGs0NFBkiiOmMUD0+oNhp9Rpk4mpNAwgA

2 个答案:

答案 0 :(得分:1)

实际上,您的第一个示例是正确推断的,第二个示例中的类型转换和数组类型有点不准确。在您的具体情况下,这无关紧要,因为您只返回第一个元素。首先,让我们看一下test

test功能

State[T]"foo"[] | "bar"[] | undefined相同。您也可以这样写:

State[T] -> State["foo" | "bar"] -> State["foo"] | State["bar"] -> "foo"[] | "bar"[] 

=> "foo"[] | "bar"[] | undefined (optional properties possible)

因此,arr在函数末尾的类型为"foo"[] | "bar"[]arr[0]类型为"foo" | "bar"的类型是正确的,因为if块排除了未定义的值。 IntelliSense显示的类型表示可能会有些混乱,因为有时它们最终会变得更加冗长/细粒度,有时会变得更加紧凑/无法解析。编译器的规范类型相同。

test2的比较

在一开始,我说过您的强制转换数组类型有点不准确。假设我们返回test1test2中的整个数组(不仅是第一个元素),以说明问题。

testtest2的新功能签名

// test signature
<T extends Supported>(state: State, type: T): State[T]
// test2 signature
<T extends Supported>(state: State, type: T): T[] | undefined

测试用例:

// define some variables
declare const state: State;
declare const stateType: "foo" | "bar"; 

// invoke functions
test(state, stateType); // return type: "foo"[] | "bar"[] | undefined
test2(state, stateType); // return type: Supported[] | undefined

结果:

const test_sample1: "foo"[] | "bar"[] | undefined = ["foo", "foo"] // works
const test_sample2: "foo"[] | "bar"[] | undefined = ["foo", "bar"] // <-- error!
const test2_sample1: Supported[] | undefined = ["foo", "bar"] // works
const test2_sample2: Supported[] | undefined = ["foo", "foo"] // works

因此,在test2中进行手动投射时,您可以返回["foo", "bar"],而test是不可能的。部分原因是,以下内容不同:

"foo"[] | "bar"[] !== ("foo"|"bar")[]

according to the documentation

希望,它会有所帮助。干杯

答案 1 :(得分:1)

我认为这里发生的事情类似于this reported issuethis Stack Overflow question ...当您从constrained generic中读取属性时(例如State[K],其中{{1 }}),泛型类型被扩展到其约束(因此K extends keyof State变成K,并且keyof State被评估为State[K],即State[keyof State]。您的情况并非完全是错误"foo"[] | "bar"[] | undefined的类型为state[type]的事实……这并不是您所特有的 d喜欢。

您想将"foo"[] | "bar"[] | undefined看作是state[type]之类的东西。但是编译器只是不为您执行此操作……它需要进行一些高级类型分析,而编译器不知道该如何执行。它必须能够计算K[] | undefined等效于Exclude<State[K], undefined>[0],而不能。

K中使用的类型断言对我来说似乎是一个合理的解决方法。

另一种可能性是将test2参数的类型从state扩展到在索引时被强制视为State的值。例如,类似K[] | undefined的东西。直接使用该参数的唯一问题是,编译器将同时使用Partial<Record<K, K[]>>type参数来推断state,并且可能会将K扩展到完整的{{ 1}}。我们只想使用K来推断keyof State,所以最好告诉编译器使用type in a "non-inferential" way中的类型参数。一种实现此目的的方法according to a language maintainer是在我们不希望进行推理的地方用K替换state。这导致我们想到这一点:

K

现在,编译器可以理解,在消除K & {}之后,function test3<K extends keyof State>( state: Partial<Record<K & {}, K[]>>, type: K ) { const arr = state[type]; // const arr: { [P in K]?: P[] | undefined; }[K] if (!arr) { return; } // const arr: K[] return arr[0]; } 将是undefined类型。因此,arr的返回类型被推断为K[]。万岁!是的,与您的类型断言相比,这要花很多时间,因此实际上,我可能只是断言并继续前进。

希望有所帮助;祝你好运!

Link to code