dndr
dndr

Reputation: 2359

Optional properties based on generic type

I'd like to make a property optional based on the generic type. I've tried the following:

interface Option<T extends 'text' | 'audio' | 'video'> {
    id: string;
    type: T;
    text: T extends 'text' ? string : undefined;
    media: T extends 'audio' | 'video' ? T : undefined;
}

const option: Option<'text'> = { text: "test", type: "text", id: "opt1" };

So the idea is that the property text only is defined for Option<'text'> and media only is defined for Option<'audio' | 'video'>.

However, the ts compiler gives me the following error:

Property 'media' is missing in type '{ text: string; type: "text"; id: string; }' 
but required in type 'Option<"text">'.ts(2741)

How can I work around this?

Upvotes: 5

Views: 2192

Answers (3)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249606

You can't have the optionality of a property depend on a generic type parameter in an interface. You can however use a type alias and intersections instead:

type Option<T extends 'text' | 'audio' | 'video'> = {
    id: string;
    type: T;
} 
& (T extends 'text' ? { text: string } : {})
& (T extends 'audio' | 'video' ? { media: T }: {});


const option: Option<'text'> = { text: "test", type: "text", id: "opt1" };

play

Although you are probably better off with a discriminated union:

type Option = 
| { id: string; type: 'text'; text: string }
| { id: string; type: 'audio' | 'video'; media: 'audio' | 'video' };


const option: Extract<Option, {type: 'text' }> = { text: "test", type: "text", id: "opt1" };

function withOption(o: Option) {
    switch(o.type) {
        case 'text': console.log(o.text); break;
        default: console.log(o.media); break;
    }
}

Playground Link

Upvotes: 10

Ibrahim AlTamimi
Ibrahim AlTamimi

Reputation: 632

It's not possible to have a generic type using string values like what you are mentioned, interface Option<T extends 'text' | 'audio' | 'video'>.

But if you want to have something like use below:

  • option.ts
interface Option {
    id: string;
    type: 'text' | 'audio' | 'video' | undefined;
}
  • media-option.ts
interface MediaOption extends Option {
    media: string;
}
  • text-option.ts
interface TextOption extends Option {
    text: string;
}

so; When you want to use them, you have to cast to a specific type between if it's media or text option based in type.

let option: Option = {id:'1',type: 'audio'}

if(option.type === 'audio'){
let media = option as MediaOption
}

Upvotes: 0

T.J. Crowder
T.J. Crowder

Reputation: 1074276

You can do it with a union:

type Option<T extends 'text' | 'audio' | 'video'> =
    {
        id: string;
        type: T;
    }
    &
    (
        T extends 'text'
        ? {text: string}
        : {media: T}
    );

const option: Option<'text'> = { text: "test", type: "text", id: "opt1" };

Playground link

Upvotes: 5

Related Questions