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
If you are interested in static type validation, you can check my blog