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

Incorrectly sets type to "never" #32375

Closed
TidyIQ opened this issue Jul 12, 2019 · 7 comments
Closed

Incorrectly sets type to "never" #32375

TidyIQ opened this issue Jul 12, 2019 · 7 comments
Labels
Duplicate An existing issue was already created

Comments

@TidyIQ
Copy link

TidyIQ commented Jul 12, 2019

TypeScript Version: 3.5.3

Search Terms: type is not assignable to type 'never', keyof, generic

Code

interface FormState {
  [key: string]: {
    fields: {
      [key: string]: {
        isInvalid: boolean;
        isValid: boolean;
        labelWidth: number;
        showPassword?: boolean;
        value: string;
      };
    };
    open?: boolean;
  };
}

let formState: FormState = {
  foo: {
    fields: {
      bar: {
        isInvalid: false,
        isValid: false,
        labelWidth: 0,
        value: ""
      }
    }
  }
};

interface SetFieldValueProps<
  T extends keyof FormState = keyof FormState,
  F extends keyof FormState[T]["fields"] = keyof FormState[T]["fields"],
  K extends keyof FormState[T]["fields"][F] = keyof FormState[T]["fields"][F]
> {
  form: T;
  field: F;
  key: K;
  value: FormState[T]["fields"][F][K];
}

const doSomething = (props: SetFieldValueProps): void => {
      const { form, field, key, value } = props;
      formState[form].fields[field][key] = value;
}

Expected behavior:

No issue. Type formState[form].fields[field][key] correctly gets the type as const key: "isInvalid" | "isValid" | "labelWidth" | "showPassword" | "value" and value correctly gets the type as const value: string | number | boolean | undefined. Nowhere in there is never defined as a possible type.

Actual behavior:

Type 'string | number | boolean | undefined' is not assignable to type 'never'.
  Type 'undefined' is not assignable to type 'never'.

Playground Link: Link to playground

@jack-williams
Copy link
Collaborator

I think this is caused by #30769, and I would start there to read up on the motivation for the changes.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Jul 12, 2019
@fatcerberus
Copy link

fatcerberus commented Jul 13, 2019

never occurs because T[K] in a write position resolves to an intersection, and the intersection of a bunch of literals is empty, therefore never.

Why this happens: the compiler knows one of several properties will be accessed, but not which one; so in order to maintain type safety it only allows you to write using that key if all the types selected overlap. This is working as designed and not a bug.

@TidyIQ
Copy link
Author

TidyIQ commented Jul 13, 2019

I'm still confused about why this isn't a bug. How else are you meant to write this without getting any errors? What changes would I need to make to get this to work?

@fatcerberus
Copy link

fatcerberus commented Jul 13, 2019

It's not a bug because prior to TS 3.5, the error below wasn't caught:

interface Foo {
    foo: string;
    bar: boolean;
}

const foo: Foo = { foo: "foo", bar: true };

function setIt(obj: Foo, key: keyof Foo, value: Foo[keyof Foo]) {
    obj[key] = value;  // in 3.5, an error; 3.4 or earlier, no error
}

setIt(foo, "bar", "oops!");
console.log(foo.bar);  // bar is typed as boolean but now contains a string

The loophole is that obj[key] used to be inferred as a union, which allowed the unsafe assignment above. Now it's inferred as an intersection in this context instead, and you get never here too because string & boolean is an empty intersection - there is no possible value you can give that will be safe for both cases.

@nattthebear
Copy link

What changes would I need to make to get this to work?

The example is very strange as written. Why is SetFieldValueProps generic when the type parameters are never used (always set to their default values)? If you actually use the generic type, it makes a lot more sense. Here's a slightly modified example that typechecks:

playground

Without the generic function, you're telling TS that value: FormState[T]["fields"][F][K]; always has to satisfy every possible key in the object at the same time.

@TidyIQ
Copy link
Author

TidyIQ commented Jul 14, 2019

Thanks @nattthebear. I did it this way as

const doSomething = <
  F extends keyof FormState[string]["fields"] = keyof FormState[string]["fields"],
  K extends keyof FormState[string]["fields"][F] = keyof FormState[string]["fields"][F]
>

seemed to me that it was the same as leaving the generics as default, since F and K are exactly the same as the default type. I see where I went wrong though. I thought Typescript would be able to infer the types for form, field, and key from the parameter passed in, but I guess it doesn't and you need to explicitly define those types.

Anyway, your solution works. Thanks for the help!

@TidyIQ TidyIQ closed this as completed Jul 14, 2019
@fatcerberus
Copy link

I thought Typescript would be able to infer the types for form, field, and key from the parameter passed in

Yeah, it doesn't, you have to pass the type parameters through manually. That's why you got the never, because FormState[T]["fields"][F][K] was selecting too many properties (i.e. all of them) and the compiler knows you can't possibly come up with a value that satisfies every single one (this wasn't caught prior to 3.5, as illustrated in my example above). Hence never 😉

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