// sanity checks
type unknownExtendsUnknown = unknown extends unknown ? true : false;
// ^?
type undefinedExtendsUnknown = undefined extends unknown ? true : false;
// ^?
type unknownExtendsUndefined = unknown extends undefined ? true : false;
// ^?
export const keyValueTypes = <T extends PropertyDescriptorMap>(
obj: T
): // prettier-ignore
{
[Key in keyof T]:
/* if */ unknown extends T[Key]['value'] ? /* then return */ T[Key]['value'] :
/* else if */ T[Key]['get'] extends (...any: any[]) => any ? /* then return */ ReturnType<T[Key]['get']> :
/* else if */ T[Key]['set'] extends (...any: any[]) => any ? /* then return */ Parameters<T[Key]['set']>[0] :
/* else return */ undefined
} => {
return {} as any;
};
const fn = () => 'hi';
const fn2 = (val: string) => {};
const x = keyValueTypes( {
// ^?
// WHY undefined and unknowns only????
v: { value: fn },
v2: { value: 5 },
getFn: { get: fn },
getFn2: { get: fn, set: fn2 },
});
#Why does my conditional return type always have undefined or unknown values?
60 messages · Page 1 of 1 (latest)
I don't know what I'm doing wrong here. I've simplified it to the point where looks straightforward to me. Why is this returning unknowns and undefined values?
!pg
!pg

This works perfectly: I don't understand!
// sanity checks
type unknownExtendsUnknown = unknown extends unknown ? true : false;
// ^?
type undefinedExtendsUnknown = undefined extends unknown ? true : false;
// ^?
type unknownExtendsUndefined = unknown extends undefined ? true : false;
// ^?
type stringExtendsUndefined = string extends undefined ? true : false;
// ^?
type stringExtendsUnknown = string extends unknown ? true : false;
// ^?
type unknownExtendsString = unknown extends string ? true : false;
// ^?
export const keyValueTypes = <T extends PropertyDescriptorMap>(
obj: T
): // prettier-ignore
{
[Key in keyof T]:
/* if */ T[Key]['get'] extends (...any: any[]) => any ? /* then return */ ReturnType<T[Key]['get']> :
/* else if */ T[Key]['set'] extends (...any: any[]) => any ? /* then return */ Parameters<T[Key]['set']>[0] :
/* else return */ T[Key]['value']
} => {
return {} as any;
};
const fn = () => 'hi';
const fn2 = (val: string) => {};
const x = keyValueTypes( {
// ^?
v: { value: fn },
v2: { value: 5 },
getFn: { get: fn },
getFn2: { get: fn, set: fn2 },
});
!pg
all I did was move the first condition to the bottom
Your first condition is unknown extends T[Key]['value']
This can never be true unless T[Key]['value'] is unknown.
So v and v2 both fails the check, and fails the subsequent getter and setter checks, ending in undefined.
For getFn and getFn2, they don't have value so T[Key]['value'] is unknown (you can verify this by changing the map type to always return T[Key]['value']), so they satisfy the first check and immediately return T[Key]['value'], which is unknown.
if that's true, how do I write a condition that says, "if this property has a value?"
oh, maybe that doesn't make sense? These are type conditions, not value conditions?
Eh yeah you are operating on the type level.
The definition is:
interface PropertyDescriptor {
configurable?: boolean;
enumerable?: boolean;
value?: any;
writable?: boolean;
get?(): any;
set?(v: any): void;
}
interface PropertyDescriptorMap {
[key: PropertyKey]: PropertyDescriptor;
}
So I guess there's no way to distinguish missing value vs value is just unknown, since both cases they return unknown.
You second solution makes sense though.
I went through an iteration that made absolutely no sense to me but seemed to work. Let me see if I can find it
Preview:ts // sanity checks type unknownExtendsUnknown = unknown extends unknown ? true : false; // ^? type undefinedExtendsUnknown = undefined extends unknown ? true : false; // ^? type unknownExtendsUndefined = unknown extends undefined ? true : false; // ^? type stringExtendsUndefined = string extends undefined ? true : false; ...
You can choose specific lines to embed by selecting them before copying the link.
ah, I was wondering if there was some way I could use infer here.
that type doesn't error locally, may be a tsconfig setting
Yeah that ensures value actually exists, and infer is just to pull out the value type.
You could also write it as:
T[Key] extends { value: unknown } ? T[Key]['value'] : never
But there's no point in avoiding infer.
right
so why does this appear to work? The longer I look at it, the more it feels like I need to negate the conditions
See this
This can never be true unless T[Key]['value'] is unknown, in the cases of value is missing (or is actually unknown), it is true so it proceeds to the branches that check for getter and setter.
And that also highlights an issue with it, that it cannot properly handle value of unknown.
right, but why does it work correctly then?
When value is missing, it gives back unknown, so unknown extends T[Key]['value'] passes, and your code goes on to check for getter and setter.
But "value is missing -> gives unknown" is not something you should rely on.
btw, to me that seems unintuitive
yeah
how did you learn that? How can I figure that out on my own?
you always returned T[Key]['value']?
no conditional?
Yeah just some basic debugging
If you don't know why unknown extends T[Key]['value'] is passing when it shouldn't, easiest is to just change it to T[Key]['value'] and see what they are.
And by doing that you will find out missing value is treated as unknown.
knowing how to do that debugging is the hardest part for me in these situations
Yeah, easiest think about how you would console.log things, and just do it on the type level.
Yeah maybe it just takes experience, because sometimes I try to use that mentality and it prints typeof ComplexInterface which is rarely helpful. To be honest, I think typescript is severely lacking when it comes to providing feedback on complex types. I'm curious if you disagree
Definitely don't disagree, debugging types is not the nicest thing.
Especially hover information sometimes is very unhelpful.
Thank you, @solemn thicket