1

I’m having a hard type figuring out how to define a type as the union of all possible values from a predefined object type.

Assume we have an autogenerated type Person that looks like this:

type Person = {
  favouriteColor: string
  age: number
  female: boolean
}

How does one use the Person type to create a union type equal to string | number | boolean?

In my use case, the type Person is autogenerated. I’m using Ramda’s map function on an object, to apply a function to each of the object’s values:

import { map } from 'ramda'

classroom.people.forEach(person =>
  // Ramda’s `map` is applied to a `block` object here:
  map<Person, Person>(property => {
    // The type definitions for Ramda are not sufficiently strong to infer the type
    // of `property`, so it needs to be manually annotated.
    return someFunction(property)
  }, person)
)

The behavior I’m looking for is essentially equivalent to keyof — but as far as I know there is no valueof in TypeScript. What would an equivalent implementation look like?

Thank you very much!


Edit: ordinarily, the solution would be as suggested by @kaya3: type ValueOf<T> = T[keyof T]. However, upon closer inspection, my situation seems to be troubled by the following:

type PersonCommonFields = {
  age: number,
  name: string
}
type PersonFragment =
  | { favouriteColor: string }
  | { female: boolean }
  | { eyeColor: Color }
  | { born: Date }
type Person = PersonCommonFields & PersonFragment

In this case, ValueOf<Person> as defined above returns number | string, i.e. only the values from PersonCommonFields, ignoring PersonFragment. The expected result for this example would be number | string | boolean | Color | Date.

Would there be an alternative approach to tackle this situation?

Many (many!) thanks in advance!

Bart
  • 1,600
  • 1
  • 13
  • 29
  • 2
    From this question - [Is there a \`valueof\` similar to \`keyof\` in TypeScript?](https://stackoverflow.com/questions/49285864/is-there-a-valueof-similar-to-keyof-in-typescript) - you can do `type ValueOf = T[keyof T];`. – kaya3 Feb 05 '20 at 22:41
  • Thank you! This is the approach that I was using. However it did not seem to return the correct type if `T` itself is the union of different types — but I’ll look into it further if this definitely is the correct answer – Bart Feb 05 '20 at 22:47
  • 1
    If there's an issue with that `ValueOf` type when `T` is a union, I suggest editing the question to ask about that case. I believe `ValueOf` solves the case in the question where `T` is `Person`. – kaya3 Feb 05 '20 at 22:50
  • Thank you — done! – Bart Feb 05 '20 at 22:55
  • 1
    That looks better - just for the sake of it being a [mcve], could you write simple examples for `A` and `B`, and say what you want the result to be? I think I have a solution that should work, but it's best to get the question exact before answering, in case I'm misunderstanding it. – kaya3 Feb 05 '20 at 22:58
  • Thanks again, done again! – Bart Feb 05 '20 at 23:02
  • 1
    Turns out the idea I had doesn't work directly, but I think it should be possible. I'll have a think about it some more. – kaya3 Feb 05 '20 at 23:16

3 Answers3

1

I noticed that if you change the | to & in PersonFragment that it works (in other words, create a single type instead of a union type). It seems like you want these fields to be optional, could you use Partial with the single type (same behavior as just making each field optional)?

type PersonCommonFields = {
  age: number,
  name: string
}
type PersonFragment = Partial<{
  favouriteColor: string,
  female: boolean,
  eyeColor: Color,
  born: Date
}>
type Person = PersonCommonFields & PersonFragment;

type PersonTypes = Person[keyof Person]; // number | string | boolean | Color | Date

Edit:

@kaya3 noted in the comments that the current behavior is that at least one of the fields in PersonFragment should be set. If this is not a requirement, the above should work.

Let's say the requirement is actually that only 1 field exists, and not more? You can use the custom type XOR1 to enforce this, and the resulting object will allow you to access the keys.

// Note: Define these in your global typings file so they can be reused
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = T | U extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;

type PersonFragment = XOR<
  { favouriteColor: string },
  XOR<{ female: boolean }, XOR<{ eyeColor: Color }, { born: Date }>>
>;

1: https://stackoverflow.com/a/53229857/522877

Wex
  • 15,539
  • 10
  • 64
  • 107
  • 1
    This isn't quite the same, since the original type requires at least one of the optional properties to be present. – kaya3 Feb 05 '20 at 23:12
  • The types that I start from are autogenerated (by `graphql-codegen`), so I would have to start from those as given... – Bart Feb 05 '20 at 23:16
  • Shoot, I glossed over that in your question. This answer might help someone else, but unfortunately, it seems like it won't be any help for you. Sorry about that. – Wex Feb 05 '20 at 23:20
1

The ValueOf<T> = T[keyof T] type doesn't work how you want for union types, because keyof doesn't distribute over unions. For example, the type keyof ({foo: 1} | {bar: 2}) is never instead of 'foo' | 'bar'.

Instead, we can use a distributive conditional type, which does distribute over unions (as its name implies).

type ValueOfUnion<T> = T extends infer U ? U[keyof U] : never

type Test = ValueOfUnion<Person>
// Test = string | number | boolean | Color | Date

Playground Link

kaya3
  • 47,440
  • 4
  • 68
  • 97
0

Inspired by @Wex’s answer I found a workaround, which isn’t perfect but sufficient for now.

graphql-codegen generates the following types:

type PersonCommonFields = {
  age: number,
  name: string
}
type A = { favoriteColor: string }
type B = { female: boolean }
type C = { eyeColor: Color }
type D = { born: Date }
type PersonFragment = A | B | C | D

From this I created type Person = PersonCommonFields & PersonFragment which is used in the code. Ideally, only this last type (Person) would be used to come up with a solution, because it doesn’t need to be kept up-to-date with PersonFragment as it evolves and includes more types in its union (E, F, G etc.) over time, as the GraphQL schema evolves.

But by rearranging a bit, we can get to this:

type PersonFields =
  | Person[keyof PersonCommonFields]
  | A[keyof A]
  | B[keyof B]
  | C[keyof C]
  | D[keyof D]

which creates the desired type number | string | favoriteColor | boolean | Color | Date.

Bart
  • 1,600
  • 1
  • 13
  • 29