TypeScript 在控制流分析中泛型类型的类型缩小

tquggr8v  于 4个月前  发布在  TypeScript
关注(0)|答案(8)|浏览(51)

Bug报告
你好,

🔎 搜索词

类型缩小、泛型类型、控制流分析

🕗 版本与回归信息

(请查看Playground)尝试缩小泛型类型(联合体)时,控制流分析无法识别到缩小。我还发现这个PR:#43183 本应解决这个问题。
请填写最符合的行:

  • 我尝试的每个版本都有这种行为,我查阅了关于类型缩小的常见问题解答

⏯ Playground链接

带有相关代码的Playground链接

💻 代码

const INITIAL_STATE = {
  1: 'test1',
  2: 'test2',
};
type State = typeof INITIAL_STATE;

const stateReducer = <Type extends keyof State>(
  state: State,
  action: { type: Type; value: string } | { type: 'clear'; value: number },
) => {
  if (action.type === 'clear') {
    // action.value is "string | number", but should be "number"
    return action.value;
  }

  return {
    ...state,
    [action.type]: action.value,
  };
};

stateReducer(INITIAL_STATE, { type: 'clear', value: 0 });

🙁 实际行为

action.value is "string | number"

🙂 预期行为

应该是 "number"

deyfvvtc

deyfvvtc1#

解:$x_{1m0n1} x$ 不是一个联合体,所以不能像你期望的那样缩小。为了让它工作,你需要把它分布在通用类型 $x_{1m1n1} x$ 上。

$x_{1a0b1} x$

gr8qqesn

gr8qqesn2#

这是一个公平的问题,为什么type不是一个判别属性。这并不是说联合中的每个成分都需要一个单元类型,因为这是可区分的:

-  action: { type: Type; value: string } | { type: 'clear'; value: number },
+  action: { type: keyof State; value: string } | { type: 'clear'; value: number },

尽管Type是一个类型参数,但它被限制在一个字面类型的联合中,这使得我们无法将其识别为判别属性。条件类型确实分配了,但通过填充约束,它也有点取消泛型化,这让我们能够将其识别为判别属性。我暂时没有看到一个很好的理由,为什么我们不能说一个类型参数的约束是字面类型的有效判别...但这些事情总是会产生意想不到的影响。

dauxcl2d

dauxcl2d3#

这并不是说联盟中的每个成分都需要一个单元类型。
你这么说很有趣,因为这也是我长期以来的理解。这只是因为@RyanCavanaugh过去的评论,我才发现规则是判别式可以是一个单元类型或者一个*单元类型的联合体(这就是为什么keyof State作为一个判别式起作用的原因)。所以我当然不会责怪那些没有意识到这一点的人。
有趣的是,那个条件类型技巧确实有效。我原以为它会推迟,因为它是在一个类型参数上分配的。

kkih6yb8

kkih6yb84#

这是一个展示我认为是同样问题的另一个玩具示例:

interface UpdateReport<B extends boolean = true> {
    newValue: string;
    oldValue: B extends true ? string : undefined;
}
export const processReport = function<B extends boolean>(
    report: UpdateReport<B>,
    hasOldValue: readonly [B],
) {
    if(hasOldValue[0]) {
        //Error TS(2345): Argument of type 'UpdateReport<B>'
        //is not assignable to parameter of type 'UpdateReport<true>'.
        //Type 'B' is not assignable to type 'true'.
        //Type 'boolean' is not assignable to type 'true'.
        //However, within this conditional check / type guard,
        //TS should be able to figure out that B is 'true'.
        let oldValue = getOldValueFromReport(report); //...
    }
}
const getOldValueFromReport = function(
    report: UpdateReport<true>
) {
    return report.oldValue;
};

搜索词:类型保护缩小,可分配给类型为2345的参数

uurity8g

uurity8g5#

另一个在使用泛型和联合时的例子。

type BcryptConfig = {
  rounds: number
  type: 'bcrypt'
}

type ArgonConfig = {
  variant: number
  type: 'argon'
}

function defineConfig<T extends { [key: string]: ArgonConfig | BcryptConfig }>(config: T): T {
  return config
}

defineConfig({
  passwords: {
    type: 'argon',
    variant: 1,
    rounds: 1,
  }
})

我希望 rounds: 1 参数被禁止,因为该属性在 ArgonConfig 上不存在。

guz6ccqo

guz6ccqo6#

关于这个问题的更新?我发现自己经常遇到这个问题,尤其是在处理像事件这样的东西时,有多种不同类型和不同有效载荷。这里有一个例子:
(ts playground)

type EventType = 'NEW_MESSAGE' | 'NEW_USER'

type AppEvent =
  | { type: 'NEW_MESSAGE'; message: string }
  | { type: 'NEW_USER'; user: string }

function fetchEvents<Type extends EventType>(
  eventType: Type
): Extract<AppEvent, { type: Type }> {
  if (eventType === 'NEW_MESSAGE') {
    // this errors, because typescript can't figure out that Type must be 'NEW_MESSAGE'
    return {
      type: 'NEW_MESSAGE',
      message: 'hello, world',
    }
  } else if (eventType === 'NEW_USER') {
    // this errors, because typescript can't figure out that Type must be 'NEW_USER'
    return {
      type: 'NEW_USER',
      user: 'rayzr',
    }
  } else {
    throw new Error(`Unknown event type: ${eventType}`)
  }
}

我经常这样做,当为处理多种类型的输入/输出创建抽象时,需要进行类型转换返回类型是很痛苦的。

r6l8ljro

r6l8ljro7#

这看起来和上面一样,只是泛型被约束在除了联合体之外的其他东西上...但它仍然应该可以区分:

type Foo<T extends {}> =
    { a: undefined, b: string } | { a: T, b: number }

function foo<T extends {}>(f: Foo<T>) {
    if (f.a == null) {
        f.b // should be string, is actually string | number
    }
}

Playground链接

6qftjkof

6qftjkof8#

我认为这是一个稍微不同的控制流示例,它没有缩小泛型,只是它不是一个属性访问:

declare function takesString(value: string): void

const data = { a: 'foo', b: 1 }
type Data = typeof data

function getValue_generic<K extends keyof Data>(key: K) {
   if (key === 'a') {
      key // K extends "a" | "b"
      const value = data[key] // { a: string; b: number }[K]
      takesString(value) // Argument of type 'string | number' is not assignable to parameter of type 'string'.
   }
}

function getValue_non_generic(key: keyof Data) {
   if (key === 'a') {
      key // "a"
      const value = data[key] // string
      takesString(value) // ok
   }
}

Playground

相关问题