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

Spread / Flatten Types #31192

Closed
5 tasks done
ClickerMonkey opened this issue May 1, 2019 · 7 comments
Closed
5 tasks done

Spread / Flatten Types #31192

ClickerMonkey opened this issue May 1, 2019 · 7 comments

Comments

@ClickerMonkey
Copy link

Search Terms

  • spread types
  • flatten types

Suggestion

Allow known nested object types to be spread & merged.

If you have a type that meets the condition (or something similar) T extends { [P in keyof T]: object } I want to be able to flatten it like so ...T which merges all sub-object types into one type.

Use Cases

What do you want to use this for?

To create better types for popular libraries (namely Vuex at the moment). This would also help make typesafe many utility functions which perform this same function of flattening objects.

What shortcomings exist with current approaches?

I don't think it's possible at the moment.

Examples

// simple
type SimpleFlatten<T> = T extends { [P in keyof T]: object } ? ...T : never;
interface SimpleFlattenTest {
  a: { b: string, c: number },
  meow: { b: number, d: boolean }
}
SimpleFlatten<SimpleFlattenTest>; // { b: string | number, c: number, d: boolean }
SimpleFlatten<{}>; // {}
SimpleFlatten<boolean>; // never
SimpleFlatten<{a:string}>; // never

// advanced, might not be possible (ideally it would be...)
type NestedFlatten<T> = T extends { [P in keyof T]: object } 
  ? ...{ [A in keyof T]: NestedFlatten<T[A]> } 
  : T;

interface NestedFlattenTest {
  a: { b: string, c: number },
  meow: { b: number, d: boolean },
  nested: {
    ignored: { d: string },
    taco: { c: string }
  }
}

NestedFlatten<NestedFlattenTest>; // { b: string | number, c: number | string, d: boolean | string }
NestedFlatten<{}>; // {}
NestedFlatten<boolean>; // boolean
NestedFlatten<{a:string}>; // {a: string}

Checklist

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@jcalz
Copy link
Contributor

jcalz commented May 1, 2019

This isn't precisely what you're asking for, but it's as close as I could come:

type ObjKeyof<T> = T extends object ? keyof T : never
type KeyofKeyof<T> = ObjKeyof<T> | { [K in keyof T]: ObjKeyof<T[K]> }[keyof T]
type StripNever<T> = Pick<T, { [K in keyof T]: [T[K]] extends [never] ? never : K }[keyof T]>;
type Lookup<T, K> = T extends any ? K extends keyof T ? T[K] : never : never
type SimpleFlatten<T> = T extends object ? StripNever<{ [K in KeyofKeyof<T>]:
  Exclude<K extends keyof T ? T[K] : never, object> |
  { [P in keyof T]: Lookup<T[P], K> }[keyof T]
}> : T
type NestedFlatten<T> = SimpleFlatten<SimpleFlatten<
  SimpleFlatten<SimpleFlatten<SimpleFlatten<T>>>>>;

interface SimpleFlattenTest {
  a: { b: string, c: number },
  meow: { b: number, d: boolean }
}

type S1 = SimpleFlatten<SimpleFlattenTest>; // { b: string | number, c: number, d: boolean }
type S2 = SimpleFlatten<{}>; // {}

// differs here:
type S3 = SimpleFlatten<boolean>; // boolean, not never
// differs here:
type S4 = SimpleFlatten<{ a: string }>; // {a: string}, not never 

interface NestedFlattenTest {
  a: { b: string, c: number },
  meow: { b: number, d: boolean },
  nested: {
    ignored: { d: string },
    taco: { c: string }
  }
}

type N1 = NestedFlatten<NestedFlattenTest>; // { 
// b: string | number, c: number | string, d: boolean | string }
type N2 = NestedFlatten<{}>; // {}
type N3 = NestedFlatten<boolean>; // boolean
type N4 = NestedFlatten<{ a: string }>; // {a: string}

There are big caveats:

  • The possible edge cases boggle my mind, so who knows what will happen if you poke at this with recursive/optional/other properties or types.
  • The "nested" case is not truly nested but only goes down five or so levels. Recursive/circular types where the recursion is not hidden by property accesses are not supported. There have been tricks that fool the compiler into trying to evaluate them (mostly by pretending that the type is one of these supported tree-like types and then indexing into them) but they scare me and are not recommended.
  • Even the five or so levels of this stuff involves a lot of compiler processing. It's a bunch of work to drill down into two layers of objects and pull everything from level 2 up into level 1 in the appropriate way. And the extra work to keep applying is probably exponential in the depth of the tree (at least the way I've got it implemented above), so I'd stay away from it even though it's a finite depth.

Proceed with caution, I guess.

@ClickerMonkey
Copy link
Author

That solves my particular problem... I'll be careful and hope my users share the same precautions.

Just when you think you understand how to Typescript...

@ClickerMonkey
Copy link
Author

@jcalz I've ran into another problem, and your solution above doesn't really address. Maybe I'm missing something? If I'm not, maybe there is some utility in this feature suggestion.

I have the following type:

type Funcs = {
  a: () => void;
  v: (text: string) => number;
}

And if I flatten/spread it, I would expect the following:

type FlattenOutput = {
  () => void;
  (text: string) => number;
};

Any pointers would be greatly appreciated, I understand about 80% of your types above... so if this sort of flattening is possible like above I'm failing at figuring it out.

@ClickerMonkey
Copy link
Author

Essentially the spread/flatten operator would take an object and for each property apply & to each one.

@jcalz
Copy link
Contributor

jcalz commented May 2, 2019

That is a different operation from what you were describing before, but here:

type IntersectProperties<T> = T extends object
  ? { [K in keyof T]: (arg: T[K]) => void } extends Record<
      keyof T,
      (arg: infer A) => void
    >
    ? A
    : never
  : T;

type Funcs = {
  a: () => void;
  v: (text: string) => number;
};
type IPFuncs = IntersectProperties<Funcs>;
// type IPFuncs = (() => void) & ((text: string) => number) 👍

type Foo = { a: string; b: number };
type Bar = IntersectProperties<Foo>;
// type Bar = string & number 😕

Note how that does bizarre things to non-function properties, so you'd have to combine this with the other type functions somehow.


I am deeply skeptical that you could actually implement a function that turns a general object with function-valued properties into a single overloaded function, though. How do you select which function to call at runtime?

declare function magic<F extends Record<keyof F, Function>>(
  funcs: F
): IntersectProperties<F>; 
// how do you implement this

interface Funky {
  a: (a: string) => number;
  b: (b: number) => string;
}
const funky: Funky = {
  a: s => s.length,
  b: n => n + "!"
};

const hmm = magic(funky);
hmm(1); // "1!"
hmm("a"); // 1

You'd probably have to pass argument type guard functions along with your function object in order for that to work. So, uh, yeah.

Anyway, I don't think this discussion is best had here, since you'd first want to demonstrate that the feature you want is not implementable without a change to the language (and that your use case is compelling enough that it's worth doing). Questions about how to implement particular type functions probably belong on Stack Overflow.

@ClickerMonkey
Copy link
Author

Thanks.

Your code above led me to type Intersect<T> = T extends { [K in keyof T]: infer E } ? E : T; which essentially does the same thing as I wanted, except with the | operator.

It would be nice if it were possible with the & operator at some point, but your solutions address my immediate need.

@captaincaius
Copy link

@jcalz your code for IntersectProperties above is exactly what I was looking for, so thank you!

I think this should totally be on the "Advanced Types" section of the guide (along with the Intersect above too as a stepping stone, but with a different name).

I say that not only because of its value in utility, but also because of its instructional value if it also comes with:

  • an explanation of how it works
  • the mindset involved for constructing it out of its building blocks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants