镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Type inference for object literals with string types and Object.assign #17943

Description

@w0rp

TypeScript Version: 2.4.0

I didn't know if there was an issue open for this already, but this is a tricky one. Consider the following code.

interface Tagged {
    type: 'abc'
}

const x: Tagged = Object.assign({ type: 'abc' }, {})

This code will not compile, because the type inference for the literal {type: 'abc'} ends when being passed to a function call and the type is inferred as {type: string}, which is of course not assignable to {type: 'abc'}, which is a good thing.

Expected behavior:

I expect the literal to instead be inferred as {type: 'abc'}, so the object can be assigned.

Actual behavior:

The literal is inferred as {type: string}, so assignment cannot be done.

Rationale:

Literal types like 'foo' and 3 are narrower types than string and number, so better type checking can be done with those types. If TypeScript can infer narrower types where possible, then assignments like the above will work.

There is a problem with assignments like the following, however.

const x = {type: 'abc'}
const y = Object.assign({type: 'abc'})

If TypeScript were to start inferring {type: 'abc'} in these cases instead of {type: string}, then this could break existing code. So this isn't an easy issue to handle.

Activity

  1. w0rp commented on Aug 21, 2017

    @w0rp
    Author

    There is a way to work around this in some cases. You can replace {type: 'abc'} with {type: 'abc'} as {type: 'abc'}, or write some wrapper functions.

  2. kitsonk commented on Aug 21, 2017

    @kitsonk
    Contributor

    Or this:

    const x = Object.assign({ type: 'abc' }, {}) as Tagged;

    This is expressly what type coercion is designed for, for dealing with situations where narrower types cannot be reliably contextually inferred.

  3. w0rp commented on Aug 21, 2017

    @w0rp
    Author

    Yes, that will also work in this case. In my case, I had some existing objects that weren't tagged, and I had some new types where I was creating objects which have tags in them. So the arguments were [{type: 'abc'}, T] producing T & {type: 'abc'}.

    I'm wondering if it would be possible to have TypeScript figure out these kinds of expressions automatically. It certainly wouldn't be easy. It might be impossible.

  4. jcalz commented on Aug 21, 2017

    @jcalz
    Contributor

    Yeah it would be great if TypeScript allowed you to annotate an object literal in a way that allowed it to choose the narrowest type possible so you don't have to repeat yourself. Here's something I sometimes use (as a library) when repeating myself feels worse than jumping through hoops:

    class LiterallyTypedObjectBuilder<T> {
      obj = {} as T;
      private constructor() {
      }
      and<K extends string, V extends string | number | boolean | {}>(k: K, v: V): LiterallyTypedObjectBuilder<T & Record<K, V>> {
        var that = this as any as LiterallyTypedObjectBuilder<T & Record<K,V>>
        that.obj[k] = v;
        return that;
      }
      build(): T {
        return this.obj;
      }
      static of<K extends string, V extends string | number | boolean | {}>(k: K, v: V): LiterallyTypedObjectBuilder<Record<K,V>> {
        return new LiterallyTypedObjectBuilder<{}>().and(k,v);
      }
    }

    You'd use it like this:

    interface Tagged {
        type: 'abc'
    }
    
    const x: Tagged = LiterallyTypedObjectBuilder.of('type', 'abc').build();
    const x2: Tagged = Object.assign(LiterallyTypedObjectBuilder.of('type', 'abc').build(), {});
    
    interface Another {
      type: 'def'
      size: 'S' | 'M' | 'L'
      age: number
      happy: boolean
    }
    
    const y = LiterallyTypedObjectBuilder.
      of('type', 'def').
      and('size', 'M').
      and('age', 42).
      and('happy', false).
      build();
    const yAsAnother: Another = y;

    Ugly and obnoxious, but it does infer the literals.

  5. mhegazy commented on Aug 21, 2017

    @mhegazy
    Contributor

    it becomes a chicken-and-egg problem, since the compiler needs to know the types of the inputs to find the return type, but the return type is what provides the contextual type to inform the inference.
    related to #11152

  6. mhegazy commented on Sep 6, 2017

    @mhegazy
    Contributor

    Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

  7. locked and limited conversation to collaborators on Jun 14, 2018
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

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions