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

Add a Spread/Assign utility types -- Better type for Object.assign(), and spread type operator #51761

Closed
Mike96Angelo opened this issue Dec 5, 2022 · 7 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@Mike96Angelo
Copy link

Mike96Angelo commented Dec 5, 2022

lib Update Request

  • OptionalKeys: Results in a union of keys that are defined as optional using ?: syntax
  • RequiredKeys: The inverse of OptionalKeys
  • FlattenIntersection: Flattens an intersections. (e.g. { a: string } & { b: string } becomes { a: string, b: string })
  • Assign: Resulting type of applying types from left to right

TypeScript Playground

Sample Code

OptionalKeys, RequiredKeys:

// Results in a union of keys that are defined as optional using ?: syntax
type OptionalKeys<T> = Exclude<
  {
    [K in keyof T]: T extends Record<K, T[K]> ? never : K;
  }[keyof T],
  undefined
>;

// The inverse of OptionalKeys
type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;

FlattenIntersection:

// Flattens an intersections. (e.g. `{ a: string } & { b: string }` becomes `{ a: string, b: string }`)
type FlattenIntersection<T> = T extends infer U
  ? {
      [P in keyof U]: U[P];
    }
  : T;

Assign:

// this is a helper for Assign could be inlined but written this way for clarity
type __AssignNext<Acc, Next> = FlattenIntersection<
  {
    // props from Acc that aren't in Next
    [K in Exclude<keyof Acc, keyof Next>]: Acc[K];
  } & {
    // optional props from Next that overlap props from Acc
    [K in Extract<keyof Acc, OptionalKeys<Next>>]?: Acc[K] | Next[K];
  } & {
    // required props from Next that overlap props from Acc
    [K in Extract<keyof Acc, RequiredKeys<Next>>]: Next[K];
  } & {
    // props from Next that aren't in Acc
    [K in Exclude<keyof Next, keyof Acc>]: Next[K];
  }
>;

// Resulting type of applying types from left to right
type Assign<A extends any[]> = A extends [infer Acc]
  ? Acc
  : A extends [infer Acc, infer Next]
  ? __AssignNext<Acc, Next>
  : A extends [infer Acc, infer Next, ...infer Rest]
  ? Assign<[ Assign<[Acc, Next]>, ...Rest]>
  : never;

Assign Usage:

type OptionalObj = { a?: string }
type RequiredObj = { a: string }

type OptionalFirst = Assign<[OptionalObj, RequiredObj]>;
//   ^? { a: string }

type OptionalLast = Assign<[RequiredObj, OptionalObj]>;
//   ^? { a?: string | undefined }


// could also define Object.assign using the Assign utility type:
interface ObjectConstructor {
  assign<T extends {}, S extends any[]>(
    target: T,
    ...sources: S
  ): Assign<[T, ...S]>;
}

const optional: OptionalObj = { a: undefined }
const required: RequiredObj = { a: 'test' }

const optionalFirst = Object.assign({}, optional, required)
//    ^? { a: string }
console.log(optionalFirst) // { a: 'test' }

const optionalLast = Object.assign({}, required, optional)
//    ^? { a?: string | undefined }
console.log(optionalLast) // { a: undefined }
@MartinJohns
Copy link
Contributor

See #39522 (comment):

We've opted to not include utility type aliases in the lib unless they're required for declaration emit purposes.

@Mike96Angelo
Copy link
Author

I think this is a bit different in that it greatly improves the types for Object.assign

@MartinJohns
Copy link
Contributor

For Object.assign see this comment and the linked issues: #45215 (comment)

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Declined The issue was declined as something which matches the TypeScript vision labels Dec 5, 2022
@Mike96Angelo
Copy link
Author

The Assign utility type I've provided here fixes the issue found: #45215 (comment). I think that this proposal is worth consideration

@RyanCavanaugh
Copy link
Member

Please consider it considered. We don't add utility types to the lib, and don't really want to mess with Object.assign's type unless it's to adopt a proper spread type operator should that feature ever come to pass.

@Mike96Angelo
Copy link
Author

Mike96Angelo commented Dec 5, 2022

We don't add utility types to the lib

https://www.typescriptlang.org/docs/handbook/utility-types.html ?

The spread type operator is a neat Idea

type A = { a: string; };
type B = { b?: number ; };

type AB = { ...A; ...B; };
//   ^? { a: string; b?: number; }

My __AssignNext<Acc, Next> type though poorly named is logically equivalent to { ...Acc; ...Next; }
Perhaps a better proposal would be to add a single utility type Spread

type OptionalKeys<T> = Exclude<
  {
    [K in keyof T]: T extends Record<K, T[K]> ? never : K;
  }[keyof T],
  undefined
>;

type RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;

type Spread<A extends any[]> = A extends [infer Acc] 
  ? Acc
  : A extends [infer Acc, infer Next]
  ? {
      // props from Acc that aren't in Next
      [K in Exclude<keyof Acc, keyof Next>]: Acc[K];
    } & {
      // optional props from Next that overlap props from Acc
      [K in Extract<keyof Acc, OptionalKeys<Next>>]?: Acc[K] | Next[K];
    } & {
      // required props from Next that overlap props from Acc
      [K in Extract<keyof Acc, RequiredKeys<Next>>]: Next[K];
    } & {
      // props from Next that aren't in Acc
      [K in Exclude<keyof Next, keyof Acc>]: Next[K];
    } extends infer U
    ? {
        [P in keyof U]: U[P];
      }
    : never
  : A extends [infer Acc, infer Next, ...infer Rest]
  ? Spread<[Spread<[Acc, Next]>, ...Rest]>
  : never;

Then we could use Spread as the implementation of proper spread type operator.

type AB = { ...A; ...B; };
type ABC = { prop1: string; ...A; prop2: string; ...B; prop3: string; };

Just becomes syntactic sugar for:

type AB = Spread<[A, B]>;
type ABC = Spread<[{ prop1: string; }, A, { prop2: string; }, B, { prop3: string; }]>;

Though I think proper spread type operator should work how the JS object spread operator works so there are a few cases where Spread breaks down, currently it doesn't work with arrays/tuples, and then there's the concept in JS where spread object A into object B only copies A's own properties and not any that it inherits from its prototype chain. I'm not sure there are ways to do that in TS or not.

If proper spread type operator was a thing, I still think the Spread utility type would make for better typing for Object.assign

interface ObjectConstructor {
  assign<T extends {}, S extends any[]>(target: T, ...sources: S): Spread<[T, ...S]>;
}

instead of

interface ObjectConstructor {
  assign<T extends {}, S1>(target: T, s1: S1): { ...T; ...S1; };
  assign<T extends {}, S1, S2>(target: T, s1: S1, s2: S2): { ...T; ...S1; ...S2; };
  assign<T extends {}, S1, S2, S3>(target: T, s1: S1, s2: S2, s3: S3): { ...T; ...S1; ...S2; ...S3; };
  assign(target: object, ...sources: any[]): any;
}

Using Spread utility type gives us correct types for any number of argument where as just using proper type spread operator only gives correct types for some arbitrary length of arguments.

@Mike96Angelo Mike96Angelo changed the title Add some utility types: OptionalKeys, RequiredKeys, FlattenIntersection, Assign Add a Spread/Assign utility types -- Better types for Object.assign() Dec 6, 2022
@Mike96Angelo Mike96Angelo changed the title Add a Spread/Assign utility types -- Better types for Object.assign() Add a Spread/Assign utility types -- Better type for Object.assign(), and spread type operator Dec 6, 2022
@typescript-bot
Copy link
Collaborator

This issue has been marked as "Declined" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants