Home > Software engineering >  Configure a TypeScript type for an object to have at least 1 of any specific keys
Configure a TypeScript type for an object to have at least 1 of any specific keys

Time:05-18

This question is kinda difficult for me to explain and thus difficult for me to find a definitive answer for.

I have the below code:

type myType = {
    apple ?: any
    banana ?: any
}

const sample = function(myObject : myType) {
    if (typeof myObject.apple !== 'undefined' || typeof myObject.banana !== 'undefined') {
        // Do something
    } else {
        // This will cause an error!
    }
}

sample({ apple: true }) // This is good
sample({ banana: 'hello world' }) // This is good
sample({ apple: false, banana: 'foo bar' }) // This is good
sample({}) // This is NOT good
sample({ banana: undefined }) // This is NOT good
sample({ taco: 'hello world' }) // This is NOT good

I am trying to make a type that can detect the "NOT good" cases

My Question: How can I modify myType to trigger an error when all of its keys are not set but not when at least 1 of its known keys are set?


On a side note: I think I can do the below, but it seems very inelegant and probably a poor programming practice. I would prefer an alternate solution because my real code has dozens of keys and would be more than a little tedious.

type myType_1 = {
    apple : any
    banana ?: any
}
type myType_2 = {
    apple ?: any
    banana : any
}
type myType = myType_1 | myType_2

CodePudding user response:

You can do this with a helper type: TS Playground

type OneOf<T> = {
  [K in keyof T]-?: Pick<T, K> & Partial<T>
}[keyof T]

Note that for this to detect the sample({ banana: undefined }) case, the type of banana must not be any since undefined is assignable to any.

CodePudding user response:

THis question/answer is related, but hot full for your case.

In order to forbid undefined value in argument, usually you should just loop through each value and replace each undefined with never. SInce, it is impossible without type assertion create value which will corespond to never in your argument, TS throw an error if it will encounter undefined.

See example:


type ReplaceUndefined<Obj> = {
  [Prop in keyof Obj]: Obj[Prop] extends undefined ? never : Obj[Prop]
}

type Test = ReplaceUndefined<{ name: undefined }> // {name: never}

Next, we need to check whether keys of provided argument match myType keys. Usually you just need to use this condition: keyof Obj extends keyof Source.

Since we also need simultaneously check for undefined, we need combine these checks into one:


type ReplaceUndefined<Obj> = {
  [Prop in keyof Obj]: Obj[Prop] extends undefined ? never : Obj[Prop]
}

type Validation<Obj, Source> = keyof Obj extends keyof Source ? ReplaceUndefined<Obj> & Source : never

However, this is not the end. We still need to validate empty object. For this check, we can use an example from my previous answer.

Full code:

type myType = {
  apple?: any
  banana?: any
}

type AtLeastOne<Obj, Keys = keyof Obj> = Keys extends keyof Obj ? Pick<Required<Obj>, Keys> : never


type ReplaceUndefined<Obj> = {
  [Prop in keyof Obj]: Obj[Prop] extends undefined ? never : Obj[Prop]
}


type Validation<Obj, Source> = keyof Obj extends keyof Source ? AtLeastOne<ReplaceUndefined<Obj> & Source> : never

const sample = function <Obj,>(myObject: myType & Validation<Obj, myType>) {

  if (typeof myObject.apple !== 'undefined' || typeof myObject.banana !== 'undefined') {
    // Do something
  } else {
    // This will cause an error!
  }
}

sample({ apple: true }) // This is good
sample({ banana: 'hello world' }) // This is good
sample({ apple: false, banana: 'foo bar' }) // This is good
sample({}) // error
sample({ banana: undefined }) // error
sample({ taco: 'hello world' }) // error

Playground

If you are interested in static type validation, you can check my blog

  • Related