Skip to content

CFA doesn't work when using generics to associate two function arguments #50205

Closed
@k8w

Description

@k8w

Bug Report

🔎 Search Terms

CFA generic function

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about CFA and type inference

⏯ Playground Link

Playground link with relevant code

💻 Code

interface RouteParams {
  '/user': { userId: string },
  '/product': { productId: string },
  '/list': { pageSize: number, current: number },
}

function navigateTo<T extends keyof RouteParams>(url: T, params: RouteParams[T]) {
  // NOT WORK HERE !!!
  if (url === '/user') {
    console.log(params.userId)
  }
}

// WORKS WELL
navigateTo('/user', { userId: '123' });
// @ts-expect-error
navigateTo('/user', { xxxxx: '123' });

🙁 Actual behavior

Property 'userId' does not exist on type '{ userId: string; } | { productId: string; } | { pageSize: number; current: number; }'.
Property 'userId' does not exist on type '{ productId: string; }'.(2339)

CFA works well outside the function call, but not works inside the function.

🙂 Expected behavior

Compile successfully.

Activity

MartinJohns

MartinJohns commented on Aug 6, 2022

@MartinJohns
Contributor

The error is correct. You wrongly assume that T will always only be one specific key, but this is valid code:

navigateTo<'/user' | '/product'>('/user', { productId: '..' });

You want #27808.

k8w

k8w commented on Aug 6, 2022

@k8w
Author

@MartinJohns

The error is correct. You wrongly assume that T will always only be one specific key, but this is valid code:

navigateTo<'/user' | '/product'>('/user', { productId: '..' });

Actually, T here is not '/user' | '/product', because it is infered as '/user':

image

MartinJohns

MartinJohns commented on Aug 6, 2022

@MartinJohns
Contributor

Actually, T here is not '/user' | '/product', because it is infered as '/user':

It's allowed to explicitly specify the type parameter, as I did. In that case the type parameter is not inferred anymore.

But even if you let it infer the type you can have the same issue:

const path = '/user' as '/user' | '/product'
navigateTo(path, { productId: '..' });

Within the implementation of the function the compiler can't know if the type represents only one key, or multiple. So it errs on the safe side.

k8w

k8w commented on Aug 6, 2022

@k8w
Author

Within the implementation of the function the compiler can't know if the type represents only one key, or multiple. So it errs on the safe side.

T maybe multiple keys indeed, but when it checked by url === '/user', shouldn't CFA narrowed the T to single key?

For example, this works well:

interface RouteParams {
  '/user': { userId: string },
  '/product': { productId: string },
  '/list': { pageSize: number, current: number },
}

let key!: keyof RouteParams;
if (key === '/user') {
  //@ts-expect-error
  if (key === '/product') {

  }
}

So why it doesn't works inside the function ?

function navigateTo<T extends keyof RouteParams>(url: T, params: RouteParams[T]) {
  if (url === '/user') {
    //@ts-expect-error
    if (url === '/product') {

    }
  }
}
MartinJohns

MartinJohns commented on Aug 6, 2022

@MartinJohns
Contributor

T maybe multiple keys indeed, but when it checked by url === '/user', shouldn't CFA narrowed the T to single key?

No. You're only narrowing the type of the specific variable.

T can be "/user" | "/product", and url is "/user", but params is RouteParams["/product"]. This is perfectly legit to call, as I demonstrated twice.

k8w

k8w commented on Aug 6, 2022

@k8w
Author

Actually url is not narrowed at all, see this:

// 【THIS WORKS !】
function test(v: 'a' | 'b') {
  if (v === 'a') {
    //@ts-expect-error
    if (v === 'b') {

    }
  }
}

// 【WHY THIS DOESN'T WORKS ?】
function test2<T extends 'a' | 'b'>(v: T) {
  if (v === 'a') {
    //@ts-expect-error
    if (v === 'b') {

    }
  }
}
whzx5byb

whzx5byb commented on Aug 6, 2022

@whzx5byb

@k8w I'm not sure why v is still marked as T extends 'a' | 'b' but it is narrowed in fact. See the examples in #43183.

fatcerberus

fatcerberus commented on Aug 6, 2022

@fatcerberus

Union types strike again! This recent comment of mine #50145 (comment) comes to mind, specifically the last part:

I’ve found with TS that when generics fail to typecheck in cases where it looks like they should, it’s almost always because you’ve failed to account for the possibility of instantiating them with union types. Having two things typed as T sadly isn’t enough to guarantee they always hold compatible values… and the compiler will let you know about it

typescript-bot

typescript-bot commented on Aug 10, 2022

@typescript-bot
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Working as IntendedThe behavior described is the intended behavior; this is not a bug

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @k8w@fatcerberus@MartinJohns@whzx5byb@RyanCavanaugh

        Issue actions

          CFA doesn't work when using generics to associate two function arguments · Issue #50205 · microsoft/TypeScript