type-challenges-solutions
type-challenges-solutions copied to clipboard
type-challenges-solutions/en/medium-anyof
AnyOf
This project is aimed at helping you better understand how the type system works, writing your own utilities, or just having fun with the challenges.
https://ghaiklor.github.io/type-challenges-solutions/en/medium-anyof.html
Hey could you maybe add a line about the { [P in any]: never }
? Doesn't quite click for me yet.
@a-bolz hey, sure! I'll split it to the parts:
- When you are saying
any
, you are saying to the compiler that anything can be there, right? - Now, what will happen if you start iterating
any
? You will be iterating, basically, anything that could be. - By using mapped types
P in any
we are getting anything that could be inany
. - Using in the object type, it means
[P in any]
can be a key of the object with anything TypeScript can throw. - Using
never
as a value type, we are saying that nothing can be there.
So we are just saying that it is an object with all the keys that can be possible and their value types must not be set ever. Once it happens, it violates the never
type, hence breaks the compilation.
@ghaiklor, nice idea with empty object! I initially used NotEqual<T, {}>
from utility, but this makes possible to remove redundant condition.
Also I think your first idea is pretty nice (feels like loop vs recursion approach). If we split it into parts, it looks simpler than using infer
and rest
.
type Falsy = 0 | '' | false | [] | {[P in any] : never}
type IsTruthy<T> = T extends Falsy ? false : true
type AnyOf<T extends readonly any[]> = IsTruthy<T[number]> extends false ? false : true
P.S. Do we really need extends any
check? My snippet works fine with current test cases
@ilmpc hmm, not sure. Since we already have a conditional check in the type so it seems like adding a constraint on generic is redundant and you are right. Maybe it is a left-over from previous iterations 🤷🏻
I noticed that the basic version
type AnyOf<T extends any[]> = T[number] extends Falsy ? false : true
passes all tests https://shorturl.at/qHIPZ despite what's claimed here https://github.com/ghaiklor/type-challenges-solutions/blob/main/en/medium-anyof.md
Maybe in earlier versions of TypeScript it was different? 🤷♂️
I do agree that the version with infer
is better as it allows to make All
by the same principle. Distributive union does not give control over the "empty array" case (which should result in true
for all / every
).
type Any<TS extends any[]> = TS extends [infer H, ...infer RS]
? (H extends Falsy ? Any<RS> : true)
: false
type All<TS extends any[]> = TS extends [infer H, ...infer RS]
? (H extends Falsy ? false : All<RS>)
: true
Really neat solution! I couldn't figure out the {[P in any] : never}
part and ended up with an extra conditional to get there:
type Falsy = 0 | "" | [] | false;
type IsEmptyObj<O> = keyof O extends never ? true : false;
// ============= Your Code Here =============
type AnyOf<T extends readonly any[]> =
T extends [infer Head, ...infer Tail] // We have Head, Tail
? Head extends Falsy // If Head is falsey
? AnyOf<Tail> // Recursively call on Tail in case truthy to follow
: IsEmptyObj<Head> extends true // If Head is NOT falsey it may be the empty object
? AnyOf<Tail> // In which case keep looking in Tail
: true // If Head isn't falsey, or Empty Obj then we've found truthy
: false; // False if empty
type Falsy = 0 | "" | false | undefined | null | [] | { [P in any]: never };
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest] ?
First extends Falsy ?
Rest extends [] ?
false
: AnyOf<Rest>
: true
: false;
declare const a: AnyOf<[0, '', false, [], {}, undefined, null]>
here is my try
not {}
but {[P in any]: never}
because {} = everything except 'null' and 'undefined', that's definitely not falsy value sounds like
@ghaiklor Some feedback.
-
Similarly to @ilmpc 's comment I don't see use of:
I extends any ?
If you also don't see its need, I would advise to delete it otherwise it is confusing the readers. Or if it should be there then I would add explanation why. -
Also there is typo here: "But, having even a single true-y element results in true type literal" I think you meant "But, having even a single true-y element results in boolean type literal".
Also I will make a general note, in your solution doing I=T[number]
is crucial for distribution to happen on unions.
If you just did T[number]
distribution would not hold because distribution holds on generic naked types.
This is what I mean:
type A<T extends any[], I=T[number]> = I extends 1 ? true:false
type Result1 = A<[1,2]> // Result 1 is boolean
type B<T extends any[]> = T[number] extends 1 ? true:false
type Result2 = B<[1,2]> // While Result2 is false
@gmoniava read further on, I wrote:
the actual implementation for this turned out to be really quirky. I don’t like it, take a look So I’ve started thinking, can we make it more maintainable?
What you are talking about is the first version of my solution that I don't see any sense to explain it. However, answering your questions:
- So I could iterate over
T[number]
elements and get the item inI
- Yes,
boolean
. If all the items will befalse
then we know for sure it isfalse
. But if there is aboolean
, we can be sure there is at least onetrue
type.
@ghaiklor
So I could iterate over T[number] elements and get the item in I
You could iterate without I extends any ?
check also, isn't it? What cases does that check protect against?
What you are talking about is the first version of my solution that I don't see any sense to explain it
I saw that quote that you didn't like your initial solution, but regardless whether you like solution or not, it should be at least clearly written and explained isn't it?
Yes, boolean
Ok so it was typo. Btw. Russian translation has that part correctly.
Ideally the explanation would even say why use I
instead of using T[number]
directly. The general note in my previous comment is related to that.
Anyway, I was just trying to give feedback to improve the text, surely it is up to you what to take into account and what not. Good luck.
@gmoniava
You could iterate without
I extends any ?
check also, isn't it? What cases does that check protect against?
You can't. T[number] extends any ? ... : ...
will not work, you said it yourself. For distribution to work, I moved it to I = T[number]
and applied it as I extends any ? ... : ...
.
it should be at least clearly written and explained isn't it?
I'm open to a Pull Request with clearly explained draft solution if you want to have it here. But me personally, I don't want to spend time on explaining the solution that is not a final one. Especially, when totally different solutions were written and in the end of the post is not the solution from the beginning.
@ghaiklor Maybe I was not clear.
For distribution to work, I moved it to I = T[number] and applied it as I extends any ? ... : ....
I meant why use:
type AnyOf<T extends readonly any[], I = T[number]> = (
I extends any ? (I extends Falsy ? false : true) : never
) extends false
? false
: true;
instead of:
type AnyOf<T extends readonly any[], I = T[number]> = (I extends Falsy ? false : true) extends false
? false
: true;
They both pass.
@gmoniava ah, I don't remember why. If they both pass, then the other one is better, yes.
@ivan-kleshnin, your All type fails on the empty array test.
type All<TS extends any[]> = TS extends [infer H, ...infer RS]
? (H extends Falsy ? false : All<RS>)
: true
Expect<Equal<All<[]>, false>>
Ignore above. I just read the comment "Distributive union does not give control over the "empty array" case (which should result in true for all / every)."
The All type can be fixable by storing the original type being tested at the type level. Then we can distinguish whether the empty array is the base case of recursion or the original type being tested
type FalseArray = []
type FalseObject = { [P in any]: never }
type FalseNumber = 0
type FalseString = ''
type Falsy = FalseNumber | FalseString | FalseArray | FalseObject | false | undefined | null
type All<T extends readonly any[], O = T> =
T extends [infer Car, ...infer Cdr]
? Car extends Falsy
? false
: All<Cdr, O>
: O extends FalseArray
? false
: true
Expect<Equal<AllOf<[1, 'test', true, [1], { name: 'test' }, { 1: 'test' }]>, true>>
Expect<Equal<AllOf<[]>, false>>