#Why checking for `undefined` doesn't match Exclude<T, undefined>?

100 messages ยท Page 1 of 1 (latest)

rich havenBOT
#
onkeltem#0

Preview:ts function ensureDefined<T>( arg: T, thing: string ): Exclude<T, undefined> { if (arg !== undefined) { return arg } throw new Error(`${thing} must be defined`) }

runic bay
#

The logic of your ensureDefined seems to be a type predicate, yet the naming seems to suggest it's an assertion function.

#

If you don't want to write it as either type predicate or assertion function, you can change your type to:

function ensureDefined<T>(arg: T | undefined, thing: string): T {
    // ...
}
lavish harbor
#

so T | undefined

#

But I don't get why the first variant doesn't work

#

arg is anything. If anything doesn't equal to undefined, then it should not be undefined ๐Ÿ™‚

runic bay
#

Exclude<T, U> is a mapped type, there's generally no way to satisfy a mapped/conditional type return.

lavish harbor
#

ok, still no clue. But thanks

runic bay
#

Write it as a type predicate, write it as an assertion function, or change your signature to the above.

#

All of them are better than what you are trying to do.

lavish harbor
#

Well, both type predicate and assert are not convenient. They basically require either branching with if or an additional line of code

#

I was just trying to understand what's going on here

#

I will use T | undefined then

runic bay
#

How are they not convenient?

lavish harbor
#

additional code

#

well, type predicate is not needed at all here, because I guess type guards should not throw

runic bay
#

Type predicate:

lavish harbor
#

it's simply not what they exist for

rich havenBOT
#
function isDefined<T>(value: T | undefined): value is T {
    return value !== undefined
}

declare const foo: string | undefined

if (isDefined(foo)) {
    foo
//    ^? - const foo: string
} else {
    foo
//    ^? - const foo: undefined
}
runic bay
#

Assertion function:

rich havenBOT
#
function assertDefined<T>(value: T | undefined): asserts value is T {
    if (value !== undefined) return
    throw new Error()
}

declare const foo: string | undefined

assertDefined(foo)
foo
//^? - const foo: string
lavish harbor
#

see, two lines

#
assertDefined(foo)
foo
#

While you could use just one

runic bay
#

It's one line.

#

The foo line is just for showing you the type is now changed.

lavish harbor
#

year, and ensureDefined() is one line

runic bay
#

You are missing the point, assertDefined(foo) is also one line.

lavish harbor
#

but then goes foo

#

it's one more line, if you need to return it for example

#

Imagine a getter.

get foo() {
  return ensureDefined(this.#config.foo, "foo")
}
runic bay
#

Ah, sure I guess.

lavish harbor
#

Yep! Many thanks!

runic bay
#

(Mostly it's not the kind of code I would recommend, getters should usually be "dumb" and shouldn't really contain lots of logic or throw. Rather than validating every time the getter is accessed which is unnecessary, typically you would do the validation once either when this.#config was created/stored, or you don't do validation and let your consumer do it, and type predicates/assertion functions are designed to work well with both cases)

lavish harbor
#

So you either do this inside getter or outside of it.

#

And also it depends on the way how you configure the class.

First approach is to validate in the constructor, because it's the only method which invocation is guaranteed. In this case you require from the user to pass all the required configuration.

#

Second approach, that I use now, is to validate-on-demand. It allows to create not-configured object and configure it later on

runic bay
lavish harbor
#

just a part of it, yes

runic bay
#

If thatโ€™s the case, you should validate foo when config is set, rather than validate every time itโ€™s used.

lavish harbor
#

of coursse

#

and In my case it's a little more tricky than that

#

I use schema (arktype, but it could be zod). And whenever I setConfig, I perform full validation using the schema

#

Now in the getters I don't need to validate, as I know that what's in there - already validated if it was set, right?

#

So I only need to check if it's not undefuined

#

then... it's already validated

runic bay
#

Are you saying your config can change as your program runs, and Foo might not be there at the start but will be there eventually?

lavish harbor
#

And of course I don' tneed to call for ensureDefined all the time. Instead - only for props that cannot be undefined.

lavish harbor
#

so I can anytime reconfigure it and proceed

#

btw, the issue with reccurring validation in getter can be mitigated with MobX ๐Ÿ™‚ As it caches getters output.
But I reckon it an overkill here

#

But yes, I agree with you that throwing in getter looks like an anti-pattern

#

almost a side-effect

runic bay
#

Well this is not an approach I would take, because:

  • It defers finding the problem of โ€œitโ€™s supposed to be there but itโ€™s notโ€ to runtime rather than compile time.
  • If it throws, the stack trace does not help you find root of the problem at all, you kind of just have to track the state of your config in some way and walk through your whole program to understand why it ends up being undefined.
    Of course itโ€™s hard to say without more context, maybe itโ€™s the only way to do what you want, but those are reasons why itโ€™s not usually written like that.
lavish harbor
#

Sure, I see the reasoning and agree with it. This is why I output message saying which property was undefined ๐Ÿ™‚ Which can be helpful in finding the root of the problem

#

I refactored my code so many times. You know, it's just a handful of classes, but it took several months for me to end up with the current version, which is not necessary the final one. (I do this in my free time, and I don't have much of it.) I decided not to rush.
The main problem I was trying to address - is that I don't know on the moment of class creation which variables are required and which are not and for reason I had this complex logic in the constructor and actual methods, checking different branches of #config and deciding upon what is wrong and what is right. My TS definitions were abundant and also - complex.

#

For example, if I pass token - I should use it. But if I pass an instance of TokenMananger - I should use it. Branch.

#

Or there is a class called TokenStorage which just saves token in the filesystem. It's usage is optional, but if I use it - I have to configure a path to the file.

#

I have one more TS question. It's about ensureDefined . I would like to pass the prop name as the second paramater. But I cannot come up with the proper code yet

rich havenBOT
#
onkeltem#0

Preview:```ts
export function ensurePropDefined<
T,
K extends keyof T

(arg: T, prop: K) {
if (arg[prop] !== undefined) {
return arg[prop]
}
throw new Error(${prop.toString()} must be defined)
}

declare const a: {foo?: string | undefined}

const res = ensurePropDefined(a, "foo")
...```

lavish harbor
#

For some reason inferring doesn't bring the expeced result

#

Why is that?

#

And I rely on infer because I don't know how to construct the return type ๐Ÿ˜•

#

Don't mind the funciton name, it's wrong, it should be something like getDefinedProp or getDefinedPropAssert

runic bay
#

Required<T>[K]

#

(Although that's also unsafe and TS doesn't seem to catch it ๐Ÿค”)

lavish harbor
#

Thanks ๐Ÿ™‚

lavish harbor
rich havenBOT
#
onkeltem#0

Preview:```ts
export function getDefinedPropAssert<
T,
K extends keyof T

(arg: T, prop: K): Required<T>[K] {
if (arg[prop] !== undefined) {
return arg[prop]
}
throw new Error(${prop.toString()} must be defined)
}

declare const a: {foo?: string | undefined}
...```

runic bay
#

You just have to use Exclude<T[K], undefined> and cast the return.

lavish harbor
#

I see

#

ok, that's actually not very important, the case of string | undefined

runic bay
#

Although, if you use eOPT, it's uncommon to do it that way.

lavish harbor
#

because I need to only check if prop was really set, no matter its value

#

because its value - is the schema business

runic bay
#

You would instead do prop in arg.

lavish harbor
#

<T, K extends keyof T>(arg: T, prop: K)

runic bay
#

It's among the possible keys, doesn't mean the key will exist.

#

In general:

  • If you don't use eOPT, you do obj[key] !== undefined checks.
  • If you use eOPT, you do key in obj checks.
#

Without eOPT, the absence of a field is indicated by its value being undefined (and no way to distinguish {} vs { foo: undefined })

#

With eOPT, the absence of a field is indicated by its actual absence.

lavish harbor
runic bay
#

Are you writing a library, or an application?

lavish harbor
#

a lib

runic bay
#

Typically you don't want to turn on eOPT when writing a library.

lavish harbor
#

hm

#

to not surprise users? or why

runic bay
#

Because it basically forces your user to also turn on eOPT, or they will run into unsoundness.

lavish harbor
#

I see. But eOPT seems to be more flexible to me

runic bay
#

eOPT is more strict.

lavish harbor
#

yep. and more logical, sort of

#

I was not aware of its existence, otherwise I would have been using it already

#

Anyway, many thanks for your help @runic bay . I will leave the return type as Required<T>[K] because as I said - I don't need to validate values in my getters, only existence basically

#

Ha, to use prop in arg I need T to extend object