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

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

Closed
w0rp opened this issue Aug 21, 2017 · 6 comments
Closed

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

w0rp opened this issue Aug 21, 2017 · 6 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@w0rp
Copy link

w0rp commented Aug 21, 2017

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.

@w0rp
Copy link
Author

w0rp commented Aug 21, 2017

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.

@kitsonk
Copy link
Contributor

kitsonk commented Aug 21, 2017

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.

@w0rp
Copy link
Author

w0rp commented Aug 21, 2017

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.

@jcalz
Copy link
Contributor

jcalz commented Aug 21, 2017

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.

@mhegazy mhegazy added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Aug 21, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Aug 21, 2017

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

@mhegazy
Copy link
Contributor

mhegazy commented Sep 6, 2017

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

@mhegazy mhegazy closed this as completed Sep 6, 2017
@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

4 participants