Skip to content

Missing error when mistyped value overwrites property of spreaded generic type parameter #42690

Open

Description

Bug Report

🔎 Search Terms

generic argument, rest spread, false positive

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about object spread and generics

I found a few relevant issues, but none of them fully correspond to the current one:

⏯ Playground Link

Playground link with relevant code

💻 Code

interface InputProps {
  value: string;
}

const getProps = (props: InputProps): InputProps => ({
  ...props,
  value: 123, // when there is no generic type, it shows an error, as expected
});

const getPropsGenericNoSpead = <T extends InputProps>(props: T): T => {
  const newProps = {...props};
  newProps.value = 123; // also error, as expected
  return newProps;
};


const getPropsGeneric = <T extends InputProps>(props: T): T => ({
  ...props,
  value: 123, // no error here
});

🙁 Actual behavior

Typescript allows assigning incompatible types

🙂 Expected behavior

Typescript shows an error

Activity

  1. changed the title [-]False-positive when using object spread with a generic type[/-] [+]Missing error when mistyped value overwrites property of spreaded generic type parameter[/+] on Feb 8, 2021
  2. misuken-now commented on Mar 15, 2021

    @misuken-now

    I often run into the same problem and have trouble with it.

    Patterns encountered in React.

    import * as React from "react";
     
    export const Component: React.VFC<{ id?: string }> = <T extends { id?: string }>(props: T) => {
      <>
        1. <div {...props} />       ✅ OK
        2. <div id={1} />            ✅ Error
        3. <div {...props} id={1} /> 🤔 OK 
      </>;
     
      const r1: T = { ...props, id: 1 }; // 🤔 OK
     
      return null;
    }

    playground

    The cause seems to be related to the intersection type and never, as shown below.

    import * as React from "react";
    
    // generic type
    function f1<T extends { id: string; id2?: string }>(props: T) {
        return { ...props, id: 1 };
    }
    const R11 = f1({ id: "" }); // R11 = { id: string; } & { id: number; }
    const R12 = <div {...R11} />; // 🤔 OK
    
    // no generic type
    function f2(props: { id: string }) {
        return { ...props, id: 1 };
    }
    const R21 = f2({ id: "" }); // R21 = { id: number; }
    const R22 = <div {...R21} />; // ✅ Error

    playground

    { id: string; } & { id: number; } will evaluate to { id: never }.

    declare const v1: { id: never };
    const v2: { id: string } = v1; // ✅ `never` is correct because it can be assigned to a `string`.

    playground

    conditional types extends are all false, so { id: number } cannot be assigned to { id: string }.

    export type R11 = ReturnType<() => { id: number } extends { id: string } ? true : false>; // false
    export type R12 = ReturnType<() => { id?: number } extends { id: string } ? true : false>; // false
    export type R13 = ReturnType<() => { id: number } extends { id?: string } ? true : false>; // false
    export type R14 = ReturnType<() => { id?: number } extends { id?: string } ? true : false>; // false
    
    export type R21 = ReturnType<<T extends { id: string }>() => { id: number } extends T ? true : false>; // false
    export type R22 = ReturnType<<T extends { id?: string }>() => { id: number } extends T ? true : false>; // false
    export type R23 = ReturnType<<T extends { id: string }>() => { id?: number } extends T ? true : false>; // false
    export type R24 = ReturnType<<T extends { id?: string }>() => { id?: number } extends T ? true : false>; // false

    playground

    If there was a check for assignability before processing with the intersection type, it would work as expected, wouldn't it?

  3. misuken-now commented on Mar 15, 2021

    @misuken-now

    I was able to write a function to check the problem.

    function f1<T extends { id: string }, U extends { id: number }>(props: T, props2: U) {
        function checkType1<V, U>(props: U & {[P in keyof V]?: V[P]}) { return props; };
        function checkType2<V, U>(props: U & {[P in keyof V]?: V[P]}) { return props as U; };
        const R11 = checkType1({ ...props, ...props2 }); // never ✅ Argument Error
        const R12 = checkType1({ ...props, id: 1 });     // never ✅ Argument Error
        const R13 = checkType1({ ...props, id: "" });    // T & { id: string; } & { id?: string | undefined; } ✅ OK
    
        const R21 = checkType2({ ...props, ...props2 }); // T & U               ✅ Argument Error
        const R22 = checkType2({ ...props, id: 1 });     // T & { id: number; } ✅ Argument Error
        const R23 = checkType2({ ...props, id: "" });    // T & { id: string; } ✅ OK
    }

    playground

  4. vkarponen commented on Oct 4, 2021

    @vkarponen

    It would seem that the problem appears also without generics:

    function test(param: { [key: string]: (string | number) }) {
        const stringValues: { [key: string]: string } = {
            foo: 'bar',
            ...param
        };
        console.log(stringValues);
    }
    

    is falsely not considered an error but if you remove foo: 'bar', from the above then TS considers that to be an error as it should.

  5. misuken-now commented on Oct 7, 2021

    @misuken-now

    In situations where generics are not used, the use of "Object.assign" may be a solution.

    function test(param: { [key: string]: (string | number) }) {
      // Type '{ foo: string; } & { [key: string]: string | number; }' is not assignable to type '{ [key: string]: string; }'.
      // 'string' index signatures are incompatible.
      //   Type 'string | number' is not assignable to type 'string'.
      //     Type 'number' is not assignable to type 'string'.(2322)
      const stringValues: { [key: string]: string } = Object.assign({}, {
        foo: 'bar'
      }, param);
      console.log(stringValues);
    }

    playground

  6. teynar commented on Nov 14, 2022

    @teynar

    I am also experiencing this issue and I feel quite surprised since this looks like a fairly common case when using reduce with the spread operator.

    function foo<T extends string>(keys: T[]): Record<T, number> {
      return keys.reduce((acc, key) => {
        return {
          ...acc,
          [key]: 'string' // this is not a number, but there is no error
        }
      }, {} as Record<T, number>);
    }

    TypeScript Playground

  7. kumikumi commented on Aug 23, 2023

    @kumikumi

    This is probably related to the same issue:
    Playground Link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptDomain: check: Type InferenceRelated to type inference performed during signature resolution or `infer` type resolution

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions