sam256
sam256

Reputation: 1421

Typescript - Union constraint causes unwanted type widening

Another inference question. I'm trying to prevent Typescript from losing properly inferred type information when I have a union type buried in an object. Here's the situation:

Two interfaces--Foo and Moo--and their union FooMoo:

interface Foo { goo: string}
interface Moo { goo: number}
type FooMoo = Foo | Moo

const foo: Foo = {goo: "bar"}
const moo: Moo = {goo: 1}

And a mixed object that contains (at least) one of each:

const mixedObject = {
    foo,
    moo
}

Now, obviously, TS knows the correct types of foo and moo, so no problem here:

let fooToo: Foo
let mooToo: Moo

fooToo = mixedObject.foo
mooToo = mixedObject.moo

But now let's say I want to tell TS that mixedObject should be limited to only foos and moos. Well, that restriction apparently causes TS to lose its ability to discriminate between foo and moo, so the following code errors:

type ManyFooMoos = {[K:string]: FooMoo}
const mixedObjectToo: ManyFooMoos = {
    foo,
    moo
}

// TS errors here because it can't narrow foo to Foo and moo to Moo;
// it just knows they are both FooMoos
fooToo = mixedObjectToo.foo
mooToo = mixedObjectToo.moo

This same thing can come up in other ways where the constraint on mixedObject that causes TS to lose information is more indirect. For example:

interface ExportedObject {
    manyFooMoos: ManyFooMoos
} 

const exportedObject: ExportedObject = {
    manyFooMoos: mixedObject
}

fooToo = exportedObject.manyFooMoos.foo

So, my basic question is -- how can I keep the constraint on mixedObject (i.e., that all it's properties must be Foos or Moos) without losing the specific types of those Foos or Moos within mixedObject?

The full code is on this playground.

Upvotes: 1

Views: 262

Answers (1)

Aleksey L.
Aleksey L.

Reputation: 37928

You can achieve this by using identity function + generics. This will allow to keep the constraint and prevent type widening:

const createFooMoos = <T extends ManyFooMoos>(fooMoos: T) => fooMoos;

const mixedObjectToo = createFooMoos({
    foo,
    moo
}) // { foo: Foo, moo: Moo }

Playground

Upvotes: 3

Related Questions