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

Let us intersect the type of a function argument based on the value of another argument #41233

Closed
AnmSaiful opened this issue Oct 25, 2020 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@AnmSaiful
Copy link

AnmSaiful commented Oct 25, 2020

Here is a basic User class:

class User< U > {

    private _user: Partial< U > = {};

    public set< K extends keyof U >(

        key: K,
        val: U[ K ],

    ) : void {

        this._user[ key ] = val;

    }
}

And a basic Employee interface:

interface Employee {

    name: string;
    city: string;
    role: number;

}

Now, consider the following implementation:

const admin = new User< Employee >();

admin.set( "name", 10 );

The above script yields the following error which is expected.

Argument of type 'number' is not assignable to parameter of type 'string'.

Now take a look at the following implementation:

const admin = new User< Employee >();

admin.set< "name" | "role" >( "name", 10 );

The above script does not yield any error and it is also expected, as long as 10 is assignable to name or role.

To make it more clear, let's take a look at the following script:

const admin = new User< Employee >();

admin.set< "name" | "role" >( "name", true );

The above script yields the following error which is also expected.

Argument of type 'boolean' is not assignable to parameter of type 'string | number'.


Now, what I want to achieve is, 10 should not be allowed to be stored as a value of name at any mean.

For this, I need to make some changes to my code something similar to the followings:

public set< K extends keyof U >(

    key: K,
    val: U[ valueof key ],

) : void {

    this._user[ key ] = val;

}

But as long as there is nothing like valueof key, this cannot be achieved dynamically.

For testing, if you replace U[ K ] by U[ "name" ], you will see 10 is no longer allowed anymore at any form.

Many programmers solved similar problems by applying T[ keyof T ] which I think is not applicable to this case.

Thanks in advance.

Playground

@MartinJohns
Copy link
Contributor

MartinJohns commented Oct 25, 2020

Please respect the issue templates for bug reports and feature requests. :-) This will make the work for the TypeScript team much easier and they can focus on improving the language, instead of deciphering GitHub issues.


Fundamentally a duplicate of #27808.

@AnmSaiful
Copy link
Author

Hi @marcin-serwin sorry about not following the issue template. Please accept my apology.

I think my issue is slightly different than #27808 as because this is about dynamic literal types. Let me leave this as a comment there. Thanks.

@MartinJohns
Copy link
Contributor

But as long as there is nothing like valueof key, this cannot be achieved dynamically.

There already is typeof key, but this doesn't solve your actual issue that K can be a union of different keys. This is where #27808 kicks in.

@Nathan-Fenner
Copy link
Contributor

@MartinJohns this has nothing to do with #27808.

Your problem is that your set function's type is not quite correct (and TypeScript has a bit of unsoundness for convenience here - assignments are treated covariantly even when they should be treated contravariantly, and therefore doesn't catch it).

You want

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never
type ExtractField<T> = T extends { field: infer F } ? F : never;

class User<U> {
    private _user: Partial<U> = {};
    public set<K extends keyof U>(
        key: K,
        val:  ExtractField<UnionToIntersection<U extends infer OneCase ? (K extends keyof OneCase ? {field : OneCase[K]} : never) : never>>,
    ) : void {
        this._user[key] = val as any;
    }
}

Converting the union to an intersection achieves the correct variance here. Alternatively you could directly inline that function declaration to get the appropriate variance (UnionToInsersection is just a bit of a hack).

@MartinJohns
Copy link
Contributor

I'm saying that #27808 would allow this to be typed in a sound way.

@RyanCavanaugh
Copy link
Member

I am loathe to suggest it, but this is the one legitimate use case for Union to Intersection conversion: https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type

The "proper" fix would be #14520

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Oct 26, 2020
@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' 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
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants