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

Mapped type index access does not work well with Extract utility type #50103

Closed
Harpush opened this issue Jul 30, 2022 · 7 comments
Closed

Mapped type index access does not work well with Extract utility type #50103

Harpush opened this issue Jul 30, 2022 · 7 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@Harpush
Copy link

Harpush commented Jul 30, 2022

Bug Report

πŸ”Ž Search Terms

indexed access types

πŸ•— Version & Regression Information

Not a regression

⏯ Playground Link

link

πŸ’» Code

enum Type {
  A = 'a',
  B = 'b'
}

interface One {
  type: Type.A;
  x: number;
}

interface Two {
  type: Type.B;
  y: string;
}

type Both = One | Two;

type GetByType<T extends Type> = Extract<Both, {type: T}>;

const mapper: {[P in Type]: (val: GetByType<P>) => boolean} = {/* ... */};

// Doesn't work :(
const getIt = <T extends Both>(b: T) => mapper[b.type](b);
// Works?
const getIt2 = <T extends Type>(b: T, c: GetByType<T>) => mapper[b](c);
// Doesn't work :(
const getIt3 = <T extends Type>(b: GetByType<T>) => mapper[b.type](b);

πŸ™ Actual behavior

getIt and getIt3 doesn't compile although somehow getIt2 compiles.

πŸ™‚ Expected behavior

All three examples should compile or at least getIt3

@RyanCavanaugh
Copy link
Member

From TypeScript's perspective, getIt and getIt3 are not distinguishable from this version, which is unsoundly callable:

const getIt = <T extends Both>(b1: T, b2: T) => {
    mapper[b1.type](b2);
}
getIt<Both>({ type: Type.A, x: 0 }, { type: Type.B, y: "" });

4.7 added support for getIt2-style signatures, but that logic doesn't cover forms like getIt and getIt3.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Aug 3, 2022
@Harpush
Copy link
Author

Harpush commented Aug 4, 2022

@RyanCavanaugh Is there no way to make it work with a single parameter? as it is now it requires me to use getIt2 like this: getIt2(Type.B, {type: Type.B, y: '4'}); which is annoying.
Concerning unsound - getIt2 is even worse so that's not a good reason....
All the below examples compile although they will fail in runtime (the second one is especially dangerous):

getIt2<Type>(Type.A, {type: Type.B, y: '4'});
getIt2(Type.B, {type: Type.A, x: 5});

On the other hand getIt3 is totally safe as written now.
What's more your example uses two parameters while my example uses one which has no way to be unsound... You can even see at getIt3 that the type is almost correct (property) type: (T & Type.A) | (T & Type.B).
To be honest out of the three - getIt2 is the most dangerous in terms of unsoundness

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Aug 4, 2022

What's more your example uses two parameters while my example uses one which has no way to be unsound

Using multiple parameters isn't required to induce unsoundness. You could take b: { left: T, right: T }

@Harpush
Copy link
Author

Harpush commented Aug 4, 2022

What's more your example uses two parameters while my example uses one which has no way to be unsound

Using multiple parameters isn't required to induce unsoundness. You could take b: { left: T, right: T }

Which is exactly the case for getIt2 that works - so what's the difference?

@RyanCavanaugh
Copy link
Member

#47109 outlines the decision of which sorts of signatures/usages get this treatment and which don't.

@Harpush
Copy link
Author

Harpush commented Aug 4, 2022

@RyanCavanaugh so back to the original question then - is there any way to pass a single parameter and make the types work? Maybe without extract? I am kind of lost how to tackle it

@Harpush
Copy link
Author

Harpush commented Oct 29, 2022

@RyanCavanaugh After revisiting this issue I think the main issue is:

type GetByType<T extends Type> = Extract<Both, {type: T}>;

const a = <T extends Type>(value: GetByType<T>) => value.type;

I expect that value.type will be T extends Type but in reality it is (T & Type.A) | (T & Type.B). The usage works as expected and returns T but trying to use it as if it was T as part of other generics fails. I can always cast as T but seems like I shouldn't need to do it.
I believe it is because the extract. Is there a better way? Design limitation still? Maybe a bug after all? Open a new issue?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

2 participants