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

Object.Assign needs better alternatives regarding stronger type inference #47130

Closed
jpike88 opened this issue Dec 13, 2021 · 4 comments
Closed
Labels
Question An issue which isn't directly actionable in code

Comments

@jpike88
Copy link

jpike88 commented Dec 13, 2021

I have looked at other discussions somewhat related to this, offering weird tricks that don't really solve the issue. So I figured it's time to be specific about my one.

I used to have many instances of Object.assign in my codebase. The problem is that while it correctly outputs the type of the inputs to it according it's design, I wanted something that allows me to ENFORCE the type of the first argument against subsequent arguments.

I know Object.assign takes generics, but the current definitions are limited and clumsy to use. assign<T, U, V> for example is rather limited, I'd prefer something that works like assign<T, Partial<T>, Partial<T>, ...> to allow for what I'm going for.

e.g.

interface MyType {
    a: number;
}

const x: MyType = { a : 1 }; 
// Here's object x with type MyType.

// HOW THINGS CURRENTLY WORK
const y : MyType = Object.assign(x, { z: 1}); 
// type of y will be inferred to be 'MyType & {z: number}'. No error here, even though I explicitly set y to fit MyType. I get this union type from Object.assign that's just messy and while may be 'technically' correct, causes a high risk of invisible defects to occur as a typo would go unnoticed.

// WHAT I WANT TO BE ABLE TO DO:
const myVar : MyType = Object.assign<MyType>(x, { z: 1}); 
// the above would be invalid, as Object.assign would see the second object fail to fit the MyType interface.

// EVEN BETTER THEN THE ABOVE EXAMPLE
const myVar2 = mergeObjects(x, { z: 1}); 
// the above would also be invalid.
// mergeObjects looks at the first argument, and based on the type of the first argument, only allows subsequent arguments to fit the type of the first argument. this way, I don't even have to specify a type for myVar2, proper type inference will kick in and propagate to ensure maximum safety.

I have attempted to write an implementation that acts as an alternative to Object.assign, which tries to make the above behaviour happen. Important to note: it doesn't work. It's behaving practically the same way as Object.assign does. But this is just to show that I've tried to attempt a workaround, and how difficult it is to figure out:

// this function works identically to Object.assign().
// the purpose of this function is to ensure that the inputs fit the shape of the first property's type
export type UnionToIntersection<U> = (
	U extends any ? (k: U) => void : never
) extends (k: infer I) => void
	? I
	: never;

export function mergeObjects<T extends any[]>(
	...contents: T
): UnionToIntersection<T[number]> {
	return Object.assign(...contents);
}

Thanks guys, appreciate your help.

@jpike88 jpike88 changed the title Object.Assign needs better options around type inference Object.Assign needs better alternative regarding stronger type inference Dec 13, 2021
@jpike88 jpike88 changed the title Object.Assign needs better alternative regarding stronger type inference Object.Assign needs better alternatives regarding stronger type inference Dec 13, 2021
@bliddicott-scottlogic
Copy link

bliddicott-scottlogic commented Dec 13, 2021

@jpike88 in your example y does fit MyType - it has all the required members of MyType and the members have the correct types.

Remember objects can implement more than one interface - extra members will never cause a type error.

interface MyType {
    a: number;
}
const z1 : MyType = Object.assign({a:1}, {c:1}); // No error - a is present and has correct type.
const z2 : MyType = Object.assign({b:1}, {a:1}); // No error - a is present and has correct type.

const z3 : MyType = Object.assign({b:1}, {z:1}); // error - a is missing.
//Property 'a' is missing in type '{ b: number; } & { z: number; }' but required in type 'MyType'. ts(2741)

const z3 : MyType = Object.assign({b:1}, {a:false}); // error - a has the wrong type
//Type '{ b: number; } & { a: boolean; }' is not assignable to type 'MyType'.
//  Types of property 'a' are incompatible.
//    Type 'boolean' is not assignable to type 'number'. ts(2322)

@fatcerberus
Copy link

Excess property checks are purely a lint-level check and aren't part of the type system proper. You can always have more properties than the type says, because that's how subtyping works in the structural type system.

You're probably looking for Exact Types: #12936

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Dec 13, 2021
@jpike88
Copy link
Author

jpike88 commented Dec 13, 2021

Yes it's Exact Types by the looks of it... a proposal over 5 years old so I'm not going to hold my breath lol

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow or the TypeScript Discord community.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

5 participants