Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

How to add union type to object values #121

Closed
dsernst opened this issue Jul 3, 2020 · 5 comments
Closed

How to add union type to object values #121

dsernst opened this issue Jul 3, 2020 · 5 comments
Assignees
Labels
question More info is requested wiki It's nice to learn stuff
Projects

Comments

@dsernst
Copy link

dsernst commented Jul 3, 2020

🤔 Question

Given an object type like:

type Omega = { alpha: number }

How could I transform all of the values to also accept another type, e.g.:

type Mapped = { alpha: number | SomeType }

It's easy if the type is manually defined, as above.

But I'd like to make this into a transforming utility type for a library, with arbitrary objects passed in.

E.g.

type Mapped = AcceptSomeType<{ foo: number; bar: string; baz: string[] }> 

is equivalent to:

type Mapped = {
  foo: number | SomeType
  bar: string | SomeType
  baz: string[] | SomeType
}

Is this AcceptSomeType<T> function possible?

Sort of like mapValues in Lodash. I see O.MergeUp can sort of do something like this, but I'm looking for it to work with any object shape passed in, not just specified keys.

(See nandorojo/swr-firestore#21 for specific use-case)

Search tags, topics

#typescript #union #type #mapValues

@millsp millsp added question More info is requested wiki It's nice to learn stuff labels Jul 3, 2020
@millsp millsp added this to To do in Board via automation Jul 3, 2020
@millsp millsp self-assigned this Jul 3, 2020
@millsp
Copy link
Owner

millsp commented Jul 3, 2020

Hey @dsernst,

There are a few ways to do this.

  • The first one is with ts-toolbelt:
import {A, O} from 'ts-toolbelt'

type Omega = {
    foo: number
    bar: string
    baz: string[]
}

type SomeType = 'hello'

// Pick the solution you prefer the most
type mapped0 = O.Update<Omega, A.Key, A.x | SomeType>
type mapped1 = O.Update<Omega, O.Keys<O>, A.x | SomeType>

Update allows one to update existing values, or create new fields, and use the x placeholder (where x represents the original value).

  • The second one is with TypeScript's mapped type features:
type Mapper<O extends object, A> = {
    [K in keyof O]: O[K] | A
}

type Omega = {
    foo: number
    bar: string
    baz: string[]
}

type SomeType = 'hello'

type mapped0 = Mapper<Omega, SomeType>
  • But ultimately, the shortest will be:
import {A, O} from 'ts-toolbelt'

type Omega = {
    foo: number
    bar: string
    baz: string[]
}

type SomeType = 'hello'

type mapped0 = O.Unionize<Omega, {[K in A.Key]: SomeType}>

Unionize simply creates a union of the fields of 2 objects. Since our second object can have any key (A.Key), all the fields will get unionized. In contrast with Update, these last 2 solutions don't allow you choose which fields get updated. This might not be a requirement for your use-case, though.

Hope this helped, do not hesitate to ask more questions.

Cheers

@millsp millsp closed this as completed Jul 3, 2020
Board automation moved this from To do to Done Jul 3, 2020
@dsernst
Copy link
Author

dsernst commented Jul 3, 2020

Brilliant, thank you!

You are a wizard 🧙‍♂️

@millsp
Copy link
Owner

millsp commented Jul 3, 2020

It's only my pleasure! If any help is needed on your related issue, please ask :)

@nandorojo
Copy link

@pirix-gh what if you only want it to apply to certain types of fields, for example, only to number fields?

@millsp
Copy link
Owner

millsp commented Jul 8, 2020

Hola @nandorojo,

You would do this thanks to SelectKeys

import {A, O} from 'ts-toolbelt'

type Omega = {
    foo: number
    bar: string
    baz: string[]
} | {
    foo: 42
    bar: number
    baz: string
    qux: boolean | number
}

type SomeType = 'update'
type test0 = O.Update<
    Omega,
    O.SelectKeys<Omega, number>,
    A.x | SomeType
> // this uses 'extends->' by default

type test1 = O.Update<
    Omega,
    O.SelectKeys<Omega, number, 'contains->'>, // we select the fields that can contain `number`
    A.x | SomeType
>

type test2 = O.Update<
    Omega,
    O.SelectKeys<Omega, number, '<-contains'>, // we select `number` that can contain the fields
    A.x | SomeType							   // ^^^^^ no worries, I'll explain what this means
>

Before I start explaining all the things in detail, let's explain what we did here above:

  • We selected the keys of an object that match a specific condition.
  • This has yielded the keys that we wanted.
  • Those keys are then passed to Update, which updated those fields.

In this example, we made use of contains->, which, under the hood, uses Contains. Contains will return 1 if a union or an object contains fully or a part of another object or union. Here's an example:

type A = 1
type B = 1 | 2

type test3 = A.Contains<A, B> // False // Equivalent to `contains->`

type test4 = A.Contains<B, A> // True  // Equivalent to `<-contains`

Moving the arrow to the other side causes the comparison to happen the other way around. And this is true for all the comparison operators (Extends, Implements, Contains, Equals):

type test5 = A.Is<A, B, 'contains->'> // False // Equivalent to `A.Contains<A, B>`

type test6 = A.Is<A, B, '<-contains'> // True  // Equivalent to `A.Contains<B, A>`

Thanks to this, you can effectively compare the fields of an object to a type from left to right, or from right to left (A.Contains<A, B>, A.Contains<B, A>). That is exactly what we did here:

type test7 = O.SelectKeys<Omega, number, 'contains->'>

How does SelectKeys work? Basically SelectKeys goes through your whole object then the chosen comparison operator is called accordingly. In this case, the comparison operator Contains will be called over each field and ask "Does the field K of Omega contain number? If yes keep the key, if not discard it". And if you put the arrow the other way around:

type test8 = O.SelectKeys<Omega, number, '<-contains'>

Then the question will be more like "Does the number contain the field K of Omega? If yes keep the key, if not discard it". Now you should be able to understand this:

type test1 = O.Update<
    Omega,
    O.SelectKeys<Omega, number, 'contains->'>, // we select the fields that can contain `number`
    A.x | SomeType
>
// This one will not let stuff like `boolean | number` pass, because `boolean | number` cannot contain `number`

type test2 = O.Update<
    Omega,
    O.SelectKeys<Omega, number, '<-contains'>, // we select `number` that can contain the fields
    A.x | SomeType
>
// This will let stuff like `boolean | number` pass, because `number` can contain `boolean | number`

Ok this was a bit of an advanced course, but you asked for it! Let me know if you have any questions.

As a takeaway, you can take a look at the other matching operators.

Cheers

Repository owner locked and limited conversation to collaborators Feb 2, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question More info is requested wiki It's nice to learn stuff
Projects
Board
  
Done
Development

No branches or pull requests

3 participants