Repository navigation
Type inference for object literals with string types and Object.assign #17943
Description
Activity
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.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.
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]producingT & {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.
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.
- addedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
on 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 #11152Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.
- locked and limited conversation to collaborators
on Jun 14, 2018
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.
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'and3are narrower types thanstringandnumber, 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.
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.