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

For object type C, type parameter T and mapped type { [K in keyof T & C]: any }, keyof C isn't assignable to K #18538

Closed
mcortesi opened this issue Sep 17, 2017 · 14 comments
Labels
Bug A bug in TypeScript
Milestone

Comments

@mcortesi
Copy link

TypeScript Version: 2.5.2

Code

export interface ConfirmationComponentProps {
  title?: string,
}
function propTypes<OProps extends {}>() : {[K in keyof (OProps & ConfirmationComponentProps)]?: any } {
  return {
    title: null,
  };
};

Expected behavior:

Compile with no errors.

Actual behavior:

Fails with: { title: null }is not assignable to {[K in keyof (OProps & ConfirmationComponentProps)]?: any }`

The problem disappears if I remove the intersection type & and just use ConfirmationComponentProps; or if I replace OProps with a concrete type like:

function propTypes<OProps extends {}>() : {[K in keyof {x: number} & ConfirmationComponentProps]?: any } {
  return {
    title: null,
  };
};
@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Sep 18, 2017

OProps could be any type, in which case, it could have properties that you're simply not satisfying here.

I take it back; we do certain checks over mapped types and really you're not depending on the original type of OProps; it sounds like you're just trying to convey to the type system that a consumer will potentially get some other properties.

Any time that you use a type parameter that's only used in an output position, that's usually something that we see as pretty fishy. Could you on elaborate what you're trying to write?

@mcortesi
Copy link
Author

@DanielRosenwasser Yes, of course.

The example was just a simplification of the problem.

My case is that I'm doing a react component decorator (HOC); that is a function with type:

type withConfirmation<OProps> =  (c: Component<OProps>) => Component<OProps & ConfirmationModalProps>

So, it returns a new component that receives the original props (OProps) and new ones.

The problem is when I implement the decorator, there I define a new React Component (the wrapper) that expects both properties; and try to define propTypes for it:

interface ConfirmationComponentProps {
   title: string;
}

class ConfirmationComponent extends React.Component<ConfirmationComponentProps & OProps > {
   static propTypes: {
      title: PropTypes.string.;
   }
  ....
}

now, typings for react, define that propTypes has type:

type ValidationMap<T> = {[K in keyof T]?: Validator<T> };

where T are the props, in my case ConfirmationComponentProps & OProps.

As you can see, is not that I'm trying to do something fishy myself. It's just a combination of what i believe two valid usecases. On one side, having a decorator that expands the properties you may pass to the component, on the other side trying to define the propTypes of such a component.

propTypes you may argue is not necessary when you type your components with typescript, and probably will remove then later. But I'm performing a migration from js to typescript, so I wanted to leave them for now.

Hope it helps clarifying the need!

@jcalz
Copy link
Contributor

jcalz commented Jan 25, 2018

I think I just ran into this too.

function f<K extends string>(
  key: K, 
  recordPlain: Record<K, any>, 
  recordIntersection: Record<K, any> & {foo: string}
) {  
  // keyof recordPlain is K
  recordPlain[key]; // okay

  // keyof recordIntersection is just "foo"?  Why not K | "foo"?
  recordIntersection[key]; // error: Type 'K' cannot be used 
                           // to index type 'Record<K, any> & { foo: string; }'.
}

This makes type guards for in that narrow the object fail to behave as desired. Is this the same issue? Should I file a new one?

@mhegazy mhegazy added the Bug A bug in TypeScript label Jan 26, 2018
@mhegazy mhegazy added this to the TypeScript 2.8 milestone Jan 26, 2018
@jcalz
Copy link
Contributor

jcalz commented Feb 1, 2018

Is this also the same issue with trying to use the new inferred in type guards with generics?

function foo<T>(x: T) {
    if ("a" in x) {
        x.a // Property 'a' does not exist on type 'T'.
    }
}

Or is this something else?

@sandersn
Copy link
Member

sandersn commented Feb 1, 2018

@mcortesi An even more simplified view of the problem is assignment of the string literal "title" to K extends keyof (OProps & ConfirmationComponentProps):

function propTypes<OProps extends {}, K extends keyof (OProps & ConfirmationComponentProps)>(
    k: K, t: 'title'): K {
    k = t
    return k
};

Unfortunately, since K extends keyof (OProps & ConfirmationComponentProps), it may be a smaller union -- with fewer properties. Here are a couple of calls that show off why k = t is incorrect:

var n: never = propTypes<{}, never>('title', null as never) // 'n' now has the value 'title'!
var a: 'a' = propTypes<{ a: number }, 'a'>('title', 'a') // 'a' now has the value 'title'!

I'll see if I can think of a workaround.

@jcalz
Copy link
Contributor

jcalz commented Feb 1, 2018

But in the original issue, it's [K in keyof X]: ..., which should span all keys, not K extends keyof X, where it can be fewer.

@sandersn
Copy link
Member

sandersn commented Feb 1, 2018

Good point. But K in ... is modelled as K extends ... most places in the compiler -- it's represented internally as a TypeParameter. So it would need special-case code to behave differently, which is probably not there.

@mcortesi
Copy link
Author

mcortesi commented Feb 6, 2018

Hi! @sandersn thanks for the reply.

I agree with @jcalz since the example use in in every other example I've used it expands to all the keys. like

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

implies all keys of T must be present.

It's probably a special case, as you mention.

Nevertheless, it worths nothing that if I don't use the intersection types (&), it works:

function propTypes2() : {[K in keyof (ConfirmationComponentProps)]?: any } {
  return {
    title: null,
  };
};

An idea here, is that it might be related to the fact that, since OProps is unkwown by propTypes, keyof (OProps & ConfirmationComponentProps) means I know only the keys of ConfirmationComponentProps, but not of OProps. So it's impossible for the function to create a object with those keys. In my example it should be possible since I'm using {[K in keyof (OProps & ConfirmationComponentProps)]?: any } with the ?, so It's a partial view of that.

But it's kinda messy.

Don't know if that helps!

@sandersn
Copy link
Member

sandersn commented Feb 7, 2018

@jcalz regarding your in type guard question: narrowing only removes types from a union, and if ("a" in x) is only treated as narrowing, not as an assertion. Contrast its behaviour to instanceof, which does act as an assertion for classes with an inheritance relationship.

It's a separate issue. The first question that I think needs to be resolved is what to do when you have concrete types:

interface A { a }
interface B extends A { b }
function f(a: A) {
    if ("b" in a) {
        a // what is the type of a? A? A & { b } ? never?
    }
}
declare let a: A
declare let b: B
f(a)
f(b)

@jcalz
Copy link
Contributor

jcalz commented Feb 7, 2018

So should I file it separately or not bother?

@sandersn
Copy link
Member

sandersn commented Feb 7, 2018

@jcalz Yes, it's a good feature request. Might be impossible, but we should at least have an issue for people to explore the design space.

@bradleyayers
Copy link

I have a scenario that I think is caused by this issue, I've simplified it to the following:

interface A {
  prop: string;
}

declare let partialA: Partial<A>;
declare let a: A;

if (partialA.prop !== undefined) {
/*
    TS2322: Type 'Partial<A>' is not assignable to type 'A'.
  Types of property 'prop' are incompatible.
    Type 'string | undefined' is not assignable to type 'string'.
      Type 'undefined' is not assignable to type 'string'
*/
  a = partialA;
  a;
}

@sandersn sandersn changed the title Error when mixing keyof and intersection type and type variable For object type C, type parameter T and mapped type { [K in keyof T & C]: any }, keyof C isn't assignable to K Mar 30, 2018
@mhegazy mhegazy modified the milestones: TypeScript 3.0, Future Jul 2, 2018
@sandersn sandersn removed their assignment Jan 7, 2020
@jcalz
Copy link
Contributor

jcalz commented May 6, 2021

Was this magically fixed somewhere? I can't reproduce the issue anymore. Playground link

@sandersn
Copy link
Member

sandersn commented May 7, 2021

We've made a lot of changes to type parameters, and I believe intersections as well, in 4.3. This does indeed look fixed.

@jcalz What were you doing reproducing 4-year-old bugs???

@sandersn sandersn closed this as completed May 7, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests

6 participants