#Assert multiple object properties are not `null | undefined` at the same time

39 messages · Page 1 of 1 (latest)

dusk hedge
#

I've tried... and invoked ChatGPT... and can't QUITE do it.

Ye good old "T could be instantiated with an arbitrary type which could be unrelated to ...". I tried a few tricks I sorta know and they failed (messing with T, making obj a different type parameter that's broader so the inferred T is actually the desired output type, creating a new type parameter that has a default, and so on and so forth...)

dusty estuaryBOT
#
emtucifor#0

Preview:```ts
export const assertPropertiesDefined = <
T,
K extends keyof T

(
obj: T,
...keys: K[]
): asserts obj is Omit<T, K> & {
[P in K]-?: Exclude<T[P], null | undefined>
} => {
const errors: Error[] = []

for (const key of keys) {
if (obj[key] === undefined || obj[key] === null) {
errors.push
...```

civic moth
#

just change Omit<T, K> to T

#

you could also use NonNullable instead of Exclude there

dusk hedge
#

@civic moth Wow, there it is. What is my thinking error that I need to fix to address how I broke this? Is it because intersections are ordered and override any same properties?

dusty estuaryBOT
#
emtucifor#0

Preview:```ts
export const assertPropertiesDefined = <
T,
K extends keyof T

(
obj: T,
...keys: K[]
): asserts obj is T & {
[P in K]-?: NonNullable<T[P]>
} => {
const errors: Error[] = []

for (const key of keys) {
if (obj[key] === undefined || obj[key] === null) {
errors.push(
new Error(${String(key)} is undefined)
)
}
}

if (errors.length > 0) {
throw new AggregateError(
errors,
"Error: multiple required parameters are undefined:"
)
}
}```

dusk hedge
#

!resolved

civic moth
#

just that, you're overspecifying the result, which i guess ts can't see that it's a subtype since it's gone through a few layers

dusk hedge
#

@civic moth oops... problem. The final function ends up asserting that ALL properties are NonNullable<>, sigh...

#

assertPropertiesDefined(options) is type narrowing to Required<typeof options> instead of doing nothing (since no property names are passed in).

civic moth
#

you can change K to be an array instead of a union

dusty estuaryBOT
#
that_guy977#0

Preview:```ts
declare function assertPropertiesDefined<
T,
K extends readonly (keyof T)[]

(
obj: T,
...keys: K
): asserts obj is T & {
[P in K[number]]-?: NonNullable<T[P]>
}
declare const options: {a?: string; b: string | null}
assertPropertiesDefined(options, "b")

options
...```

dusk hedge
#

I actually tried something along those lines before posting here, but I was obviously missing other tricks.

#

Thank you! That seems to have done it!

#

Is there some way to get an error at the call site if the key passed in is neither Optional nor extends null | undefined?

#

So the IDE can help remove cruft around options that have become non-null through later changes?

#

My failed attempt:

K extends readonly (keyof { [P in keyof T]: (null | undefined) extends T[P] 
? P : never }
civic moth
#

that's still getting all the keys, since : never doesn't actually remove a property

dusk hedge
#

Ahhhhh got it

civic moth
#

something like this?

dusty estuaryBOT
#
that_guy977#0

Preview:```ts
declare function assertPropertiesDefined<
T,
K extends readonly (keyof {
[P in keyof T as null extends T[P]
? P
: undefined extends T[P]
? P
: never]: never
})[]

(
obj: T,
...keys: K
): asserts obj is T & {
[P in K[number]]-?: NonNullable<T[P]>
}
declare const options: {
a?: string
b: string | null
c: string
}
...```

civic moth
#

i think this can be made shorter

dusk hedge
#

I was wondering if the as operator would be helpful, but then I thought "oh, that's for keys", but of course a "never" key is how you exclude a property, right?

civic moth
#

ah yeah

dusty estuaryBOT
#
that_guy977#0

Preview:```ts
declare function assertPropertiesDefined<
T,
K extends readonly (keyof {
[P in keyof T as T[P] extends NonNullable<T[P]>
? never
: P]: never
})[]

(
obj: T,
...keys: K
): asserts obj is T & {
[P in K[number]]-?: NonNullable<T[P]>
}
declare const options: {
a?: string
b: string | null
c: string
}
...```

dusk hedge
#

It makes perfect sense. THANK YOU! I learned something.

#

Hopefully some of the questions people provide exhibit enough complexity and "prior effort" that they're fun to answer.

civic moth
#

they definitely do lol

#

somewhat like a challenge but not impossibly hard or based on weird circumstances

dusk hedge
#

I give people the advice all the time that any tech they want to grow in, they should answer questions online about it.

The interesting questions are always on the edge of one's ability.

#

I did it years ago to get good at one area of software development... basically, answering questions on SO and another site before that had as much to do with becoming highly proficient as my day-to-day job.

#

Oh, it's not working for objects of type Record<string, any> because any defeats type safety. Casting to Record<string, unknown> "works" but results in { prop: {} } which can't be productively used very easily... hmmm.

I guess this reveals a hole, that I never asserted the properties were a certain type other than non-null. Ugh.

civic moth
#

{} is the non-nullish type

dusk hedge
#

I'm parsing CLI options and just want to do a quick "these aren't null/undefined" before passing them to specific argument-wrappers that do the full check. But those expect strings, and previously the any just swallowed the fact that the values had no guaranteed type.

It's the commander npm package, whose output of new Command() ... .opts() is Record<string, any>. So I need to do something to either individually assert they are strings, or (after investigating) leverage my knowledge of the types they are defined as in the .option() statements to start with an initial, nullable but properly-typed-as-string-or-whatever input type.

#

Technically, the type system just made me find a hole in the assumptions, but it will be easy to fix. Unfortunately, commander doesn't have good types that would result in the opts() command having strongly-typed output object, so I'm glossing over its poor type capabilities, here.

#

The new code is better, as it forced me to not just reuse the same object, overwriting the values/types in it 🙂