type-challenges-solutions icon indicating copy to clipboard operation
type-challenges-solutions copied to clipboard

type-challenges-solutions/en/medium-anyof

Open utterances-bot opened this issue 2 years ago • 17 comments

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

utterances-bot avatar Dec 07 '21 18:12 utterances-bot

Hey could you maybe add a line about the { [P in any]: never } ? Doesn't quite click for me yet.

a-bolz avatar Dec 07 '21 18:12 a-bolz

@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 in any.
  • 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 avatar Dec 08 '21 08:12 ghaiklor

@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 avatar Jan 07 '22 11:01 ilmpc

@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 🤷🏻

ghaiklor avatar Jan 07 '22 19:01 ghaiklor

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

ivan-kleshnin avatar Jun 22 '22 06:06 ivan-kleshnin

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 

dgh500 avatar Oct 16 '22 18:10 dgh500

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

Danishsjjd avatar Nov 04 '22 17:11 Danishsjjd

not {} but {[P in any]: never} because {} = everything except 'null' and 'undefined', that's definitely not falsy value sounds like

mefengl avatar Nov 06 '22 13:11 mefengl

@ghaiklor Some feedback.

  1. 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.

  2. 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 avatar Dec 01 '22 16:12 gmoniava

@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:

  1. So I could iterate over T[number] elements and get the item in I
  2. Yes, boolean. If all the items will be false then we know for sure it is false. But if there is a boolean, we can be sure there is at least one true type.

ghaiklor avatar Dec 02 '22 09:12 ghaiklor

@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 avatar Dec 02 '22 10:12 gmoniava

@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 avatar Dec 04 '22 10:12 ghaiklor

@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 avatar Dec 04 '22 10:12 gmoniava

@gmoniava ah, I don't remember why. If they both pass, then the other one is better, yes.

ghaiklor avatar Dec 04 '22 11:12 ghaiklor

@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>>

AndrewLamWARC avatar Jan 12 '23 10:01 AndrewLamWARC

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)."

AndrewLamWARC avatar Jan 12 '23 10:01 AndrewLamWARC

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>>

AndrewLamWARC avatar Jan 12 '23 11:01 AndrewLamWARC