简体   繁体   中英

Typescript: define object of type, but infer provided optional props as required

Imagine this config object:

interface NodeConfig {
  type: 'circle' | 'square'; // must always be specified, no default value
  color?: 'red' | 'blue'; // has a default value therefore is optional on input
  effect?: 'spinning' | 'bouncing'; // is completely optional, not present = no effect
}

const defaultNodeConfig: Partial<NodeConfig> = {
  color: 'blue',
} as const;

function drawNode(config: NodeConfig) {
  const actualConfig = {...defaultNodeConfig, ...config};
  // type of actualConfig.color should be non-nullable now
  // since it was provided by defaultNodeConfig
}

I want to:

  1. guard that defaultNodeConfig conforms to NodeConfig interface, so I don't provide invalid values here (eg color: 'black' ),
  2. properly infer type of {...defaultNodeConfig, ...config} so that properties provided by default config are no more optional and I don't have to unwrap them with !

The problem is:

  • When I have specified defaultNodeConfig: Partial<NodeConfig> , it breaks the second requirement - the type is NodeConfig & Partial<NodeConfig> therefore NodeConfig , no clue about color being always present
  • When I remove the type declaration : Partial<NodeConfig> , second requirement is met as expected, but type of defaultNodeConfig is not constrained anymore so I can put {color: 'black'} to it.

Is there a way to meet both requirements, without needing to explicitly specify all the types and what should be optional at which moment? Something like const defaultNodeConfig: Partial<NodeConfig> & typeof itself (no, that syntax doesn't work) that says "Constrain the constant to meet the type, but use its inferred type when the constant is used"?

To require defaultNodeConfig to extend Partial<NodeConfig> while also being const you can use a generic function to assert the constraint. The following should work:

const defaultNodeConfig = (<T extends Partial<NodeConfig>>(v: T) => v)({
  color: 'blue',
})

This might not be as flexible as what you want for option1 but if you only define your defaultNodeConfig once then you can use a custom definition for it that matches it's content. In your example it would be

const defaultNodeConfig :Required<Pick<NodeConfig, 'color'>> = {
  color:'blue'
}

Once you declare the type of defaultNodeConfig typescript will use that when inferring other types like when you merge the two objects. In your code both objects are declared to have color as optional so the resulting type also has that definition, having a value for the optional type is legitimate and so the resulting object still allows an optional color. If you have a different default object you want to create that has both color and effect you can declare it as

const defaultNodeConfig :Required<Pick<NodeConfig, 'color'|'effect'>> = {
  color:'blue',
  effect:'spinning'
}

If you want more restrictions on defaultNodeConfig from NodeConfig you could also use

const defaultNodeConfig :Required<Pick<NodeConfig, 'color'>> & Partial<NodeConfig> = {
  color:'blue'
}

This should satisfy all of your constraints, except your merged object will still have effects as optional regardless if it is set in defaultNodeConfig

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM