Skip to content

Parameter of a generic interface doesn't work when it's a generic index of an another interface #32365

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

Closed
marcingajda opened this issue Jul 11, 2019 · 5 comments · Fixed by #53232
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@marcingajda
Copy link

TypeScript Version: typescript@next, [email protected]

Search Terms:
generic index is not assignable

Code

interface SettingsTypes {
  audio: {
    volume: string;
  };
  video: {
    resulution: string;
  }
}

interface Settings<Params extends { [K in keyof Params]?: string }> {
  config: Params;
};

// Example 1
type ThisWorks = Settings<SettingsTypes['audio']>;

// Example 2
type ThisDoesntWork<T extends keyof SettingsTypes> = Settings<SettingsTypes[T]>;

// ERROR:
// Type '{ volume: string; }' is not assignable to type '{ [K in keyof SettingsTypes[T]]?: string | undefined; }'.

// oh, ok but then why this works?

// Example 3
type ThisWorksAgain = Settings<{ volume: string; }>;

Expected behavior:
Generic should work the same if we pass an specific index of an interface (1), a generic index of an interface (2) or when we pass the type directly (3)

Actual behavior:
Typescript throws an error only for the construction Settings<SettingsTypes[T]>.

Playground Link:
https://www.typescriptlang.org/play/#code/JYOwLgpgTgZghgYwgAgMoTGUBzAzgFQE8AHCXZAbwChlk4BXAE2AHsAuSm25ANxYBt6AWwgdcYKDgDcXAL4zaPYIwjtO3ZFDL1BWFiDETpcqrKpVQkWIhTpMOXAB4ACnChwh5CAA9IIRuQUyADaANLIoMgA1hCELDDIru6eALoA-IaSINjIsgB86sgI+jDA2BxJHrgy8uYA9HXIAKLeHsT8KACMVGAkKPgAFsC4AOosUFHkALxoGFjZTnbzeESkuMEA5AzMLBspeTJUDc2tQu0oAEw9fciDwwAiLGTgYxOO+Mg+fgHRsfGz9gWqzIBRmSwcjnBQL663w+0OxyaACUkQB5JFsI6NYHIDZBPiCESZaS5DYRcggFhgOi4XBlEBwABGHWQYBYrJueJC4UiMTiCShKxhwTh6WJ2WQAB9kPR-BBSiAIIwpKSAHT1RosAYAGmQLCiyEZ9GpYAGEBAyAA7gNCKyhuRLeNJmkNSc2iyAMzXUi3e2vSYAQWwcEiYLmEPxAmEomQ4iy2BV+SkQA

Related Issues:
32017 and 31904

@marcingajda marcingajda changed the title Parameter of a generic interface doesn't when it's a generic index of another interface Parameter of a generic interface doesn't work when it's a generic index of an another interface Jul 12, 2019
@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jul 12, 2019
@jack-williams
Copy link
Collaborator

I think this is a design limitation. The issue is that you end up in a situation where you want to relate:

SettingsTypes[T] <: { [K in keyof SettingsTypes[T]]?: string }

To make any progress you need to simplify the left hand side using the constraint of T, which results in the type { volume: string } | { resolution: string }. At this point you lose the possibility of being able to relate to the right hand side because it contains the generic parameter T which you don't know whether it will be volume, resolution, or something else.

There might be some way to get this through by using some intermediate (conditional) types, but convincing the checker a priori seems hard - the level of reasoning is non-trivial.

Am I right in thinking that all you really care about is saying that Params is an object with values of type string | undefined? I wonder if there is an easier way to describe that constraint.

@marcingajda
Copy link
Author

marcingajda commented Jul 14, 2019

Hi, thanks for you response. Actually the Params part comes from an external library. See RouteComponentProps interface. However now I think I made an XY problem situation by overcomplicating things.

What I'm trying to do is I'm experimenting in my app to get named routes with typed parameters. And in fact it works great!

I have named routes:

const PAYMENTS_LIST = '/payments';
const PAYMENT_DETAILS = '/payment/:id';

I also made a single interface for all routes parameters:

interface RouteParameters {
  [PAYMENT_DETAILS]: {
    id: string;
  };
}

Finally I wrapped the formatRoute function from react-router-named-routes library to check parameters for given route:

interface MakePathFunction {
  <T extends keyof RouteParameters >(route: T, params: RouteParameters[T]): string;
  (route: string): string;
}

export const makePath: MakePathFunction = <T>(route, params = {}) => {
  return formatRoute(route, params);
};

This works like a charm.

The problem is that when I'm creating a React component with route parameters I should use the RouteComponentProps interface with proper value of Params:

const Payment: FC<RouteComponentProps<{id: string}>>

To avoid typing duplication I wanted to reuse the RouteParameters like this:

const Payment: FC<RouteComponentProps<RouteParameters[typeof PAYMENT_DETAILS]>>

It looks messy so I tried to shorter the typing with an alias:

type RCP<T extends keyof RouteParameters> = RouteComponentProps<RouteParameters[T]>;

const Payment: FC<RCP<typeof PAYMENT_DETAILS>>

And finally here the compiler was complaining about the RouteParameters[T] with a message that didn't make sense. That's why I made this issue. However if this is a design limitation then I understand it. I already found a simpler, working solution without typeof so everything is OK for me.

export interface PaymentDetailsRoute {
  id: string;
}

export interface RouteParameters {
  [PAYMENT_DETAILS]: PaymentDetailsRoute;
}

const Payment: FC<RouteComponentProps<PaymentDetailsRoute>> = (props) => {

I think the issue may be closed now. Thanks for explanation.

PS: above code is not copied 1:1 from project so it may have typos.
PS2: Made a small edit

@fatcerberus
Copy link

@jack-williams Maybe I'm missing something but:

SettingsTypes[T] <: { [K in keyof SettingsTypes[T]]?: string }

Why can't you substitute T for its constraint on both sides of the relation? It seems like that would be sound, but maybe I just haven't thought it all the way through...

@jack-williams
Copy link
Collaborator

jack-williams commented Jul 14, 2019

@marcingajda

Looking into this abit more, I believe your issue boils down to this:

type SettingsTypesKey<K extends "volume" | "resulution"> = K;
type Broken<T extends keyof SettingsTypes> = SettingsTypesKey<keyof SettingsTypes[T]>;
// Type 'string | number | symbol' does not satisfy the constraint '"volume" | "resulution"'.

Basically keyof SettingsTypes[T] ==> string | number | symbol, which causes later issues in your example because the checker needs:

  • SettingsTypes[T][string | number | symbol] <: (string | undefined)

per the comment:

// A source type T is related to a target type { [P in Q]?: X } 
// if some constituent Q' of Q is related to keyof T and T[Q'] is related to X.

but there are no index signatures on SettingsTypes[T].

Expanding T to its constraint in keyof SettingsTypes[T] looks tempting, but I believe it would be unsound:

type IsVolume<K extends "volume"> = K;
type Broken<T extends keyof SettingsTypes> = IsVolume<keyof SettingsTypes[T]>;

// If we are allowed to expand T we get:
// 1. IsVolume<keyof SettingsTypes[T]>
// 2. IsVolume<keyof SettingsTypes["audio" | "video"]>
// 3. IsVolume<keyof SettingsTypes["audio"] | SettingsTypes["video"]>
// 4. IsVolume<"volume" & "resulution"> --> this would be ok

// Yet clearly this is unsound

type Res = Broken<"video">; // resolution

So I would leave this issue open and let someone on the team have a definitive look. I'm glad you have something that works for you though!

@RyanCavanaugh RyanCavanaugh added Bug A bug in TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Aug 22, 2019
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Aug 22, 2019
@RyanCavanaugh
Copy link
Member

Minimal version:

interface SettingsTypes {
  audio: {
    volume: string;
  };
  video: {
    resolution: string;
  }
}

type SettingsTypesKey<K extends "volume" | "resolution"> = K;
type Broken<T extends keyof SettingsTypes> = SettingsTypesKey<keyof SettingsTypes[T]>;

Note that this version works:

interface SettingsTypes {
  audio: {
    volume: string;
  };
  video: {
    resolution: string;
  }
}

type SettingsTypesKey<K> = K extends "volume" | "resolution" ? K : never;
type Broken<T extends keyof SettingsTypes> = SettingsTypesKey<keyof SettingsTypes[T]>;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants