Myzel394
Myzel394

Reputation: 1317

How to create conditional interfaces without writing base classes in Typescript?

Not sure what this is called, but let's say I got these interfaces

interface Dog {
    type: "dog"
    canBark: boolean
}

interface Cat {
    type: "cat"
    canMeow: boolean
}

I would like to be able to say: If type="Dog", there should be a canBark property (as a boolean) and no other properties. If type="Cat", there should be a canMeow property (as a boolean) and no other properties.

An example React code would look like this:

export default function SomeReactComponent({ type, ...other }: Cat | Dog) {
    switch (type) {
        case "dog":
            return <DogComponent {...other} />
        case "cat":
            return <CatComponent {...other} />
        default:
            throw new Error("Unknown animal type.")
    }
}

function DogComponent({ canBark }: { canBark: boolean }) {}

function CatComponent({ canMeow }: { canBark: boolean }) {}

However, I get this error on DogComponent:

Type '{ canBark: boolean; } | { canMeow: boolean; }' is not assignable to type 'IntrinsicAttributes & { canBark: boolean; }'.
  Property 'canBark' is missing in type '{ canMeow: boolean; }' but required in type '{ canBark: boolean; }'.ts(2322)

and this error on CatComponent:

Type '{ canBark: boolean; } | { canMeow: boolean; }' is not assignable to type 'IntrinsicAttributes & { canBark: boolean; }'.
  Property 'canBark' is missing in type '{ canMeow: boolean; }' but required in type '{ canBark: boolean; }'.ts(2322)

When I create a Animal interface with all possible values, it works:

interface Animal {
    type: "dog" | "cat"
    canBark: boolean | undefined
    canMeow: boolean | undefined
}
interface Dog extends Animal {
    type: "dog"
    canBark: boolean
}

interface Cat extends Animal {
    type: "cat"
    canMeow: boolean
}

However, it's a bit tedious to write these base classes for every React component, is there a way to accomplish this without writing an Animal class?

Upvotes: 2

Views: 45

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249786

Typescript is not smart enough to follow the fact that type and other are actually entangled. There is an open issue about this one that might see improvement, as Typescript has just recently (in 4.6) added support for narrowing such entangled types with this PR

One simple option is to not de-structure the props in the parameters, narrow them first, and then pass them directly to the component or de-structure the type prop out:

export default function SomeReactComponent(o: Cat | Dog) {
    switch (o.type) {
        case "dog":
            return <DogComponent {...o} />
        case "cat":
            return <CatComponent {...o} />
        default:
            throw new Error("Unknown animal type.")
    }
}

Playground Link

You can also destructure after the narrowing:

export default function SomeReactComponent(o: Cat | Dog) {
    switch (o.type) {
        case "dog": {
            const { type, ...other } = o
            return <DogComponent {...other} />
        }
        case "cat": {
            const { type, ...other } = o
            return <CatComponent {...other} />
        }
        default:
            throw new Error("Unknown animal type.")
    }
}

Playground Link

Upvotes: 2

Related Questions