Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
k8w opened this issue Aug 6, 2022 · 9 comments
Closed

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

k8w opened this issue Aug 6, 2022 · 9 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@k8w
Copy link

k8w commented Aug 6, 2022

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.

@MartinJohns
Copy link
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
Copy link
Author

k8w commented Aug 6, 2022

@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
Copy link
Contributor

MartinJohns commented Aug 6, 2022

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
Copy link
Author

k8w commented Aug 6, 2022

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
Copy link
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
Copy link
Author

k8w commented Aug 6, 2022

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
Copy link

whzx5byb commented Aug 6, 2022

@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
Copy link

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

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Aug 8, 2022
@typescript-bot
Copy link
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
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

6 participants