Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make object property optional based on other type in TypeScript?

I think the best way to explain my scenario is with code:

interface IPluginSpec {
  name: string;
  state?: any;
}

interface IPluginOpts<PluginSpec extends IPluginSpec> {
  name: PluginSpec['name'];
  // How to require opts.initialState ONLY when PluginSpec['state'] is defined?
  initialState: PluginSpec['state'];
}

function createPlugin<PluginSpec extends IPluginSpec>(
  opts: IPluginOpts<PluginSpec>,
) {
  console.log('create plugin', opts);
}

interface IPluginOne {
  name: 'pluginOne';
  // Ideally state would be omitted here, but I can also live with having to
  // define "state: undefined" in plugins without state
  // state: undefined;
}

// Error: Property 'initialState' is missing in type...
createPlugin<IPluginOne>({
  name: 'pluginOne',
  // How to make initialState NOT required?
  // initialState: undefined,
  // How to make any non-undefined initialState invalid?
  // initialState: 'anything works here',
});

interface IPluginTwo {
  name: 'pluginTwo';
  state: number;
}

createPlugin<IPluginTwo>({
  name: 'pluginTwo',
  initialState: 0,
});
like image 317
treznik Avatar asked Jan 29 '19 07:01

treznik


People also ask

How do I make object properties optional in TypeScript?

To make a single property in a type optional, create a utility type that takes a type and the property name as parameters and constructs a new type with the specific property marked as optional.

How do you make a properties required in TypeScript?

To make an optional property required, create a utility type that uses a mapping modifier to remove the optionality for the specific property. The new type will have the specified property marked as required.

Can you restrict TypeScript object to contain only properties defined by its class?

Unfortunately this isn't currently possible in Typescript, and somewhat contradicts the shape nature of TS type checking.

How would you declare an optional member of an interface in TypeScript?

In TypeScript, the interfaces which describe objects can have optional properties. Interfaces with optional properties are written similar to other interfaces, with each optional property denoted by a ? at the end of the property name in the declaration.


1 Answers

You can do this with a conditional type. With it you can test for the existence of the property and either have or not have the extra property :

interface IPluginSpec {
  name: string;
  state?: any;
}

type IPluginOpts<PluginSpec extends IPluginSpec> = PluginSpec extends Record<'state', infer State> ? {
  name: PluginSpec['name'];
  initialState: State;
} : {
  name: PluginSpec['name']
}

function createPlugin<PluginSpec extends IPluginSpec>(
  opts: IPluginOpts<PluginSpec>,
) {
  console.log('create plugin', opts);
}

interface IPluginOne {
  name: 'pluginOne';
}

// Ok
createPlugin<IPluginOne>({
  name: 'pluginOne',
  // nothing to add
});

interface IPluginTwo {
  name: 'pluginTwo';
  state: number;
}

createPlugin<IPluginTwo>({
  name: 'pluginTwo',
  initialState: 0,
});

For a more composable approach you can use an intersection, with a common part, and each optional part in it's own conditional:

interface IPluginSpec {
    name: string;
    state?: any;
    config?: any;
}

type IPluginOpts<PluginSpec extends IPluginSpec> = {
        name: PluginSpec['name']
    }
    & (PluginSpec extends Record<'state', infer State> ? { initialState: State; } : {})
    & (PluginSpec extends Record<'config', infer Config> ? { initialConfig: Config; } : {})

The conditional type is very useful for callers. The problem is that inside the implementation, typescript can't really reason about the conditional types (since T is not known).

The best solution is to keep a public signature (with conditional types) and a simplified implementation signature (without conditional types). This will let you implement the function without type assertions while giving the caller the desired behavior:

function createPlugin<PluginSpec extends IPluginSpec>(opts: IPluginOpts<PluginSpec>)
function createPlugin<PluginSpec extends IPluginSpec>(opts: {
    name: string
    initalState: PluginSpec['state'],
    initialConfig: PluginSpec['config'],
}) {
    if (opts.initalState) {
        opts
    }
}
like image 149
Titian Cernicova-Dragomir Avatar answered Sep 20 '22 23:09

Titian Cernicova-Dragomir