Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OT - union type to array or infer how many types are in the union #80

Closed
cancerberoSgx opened this issue Jan 31, 2019 · 9 comments
Closed

Comments

@cancerberoSgx
Copy link

Sorry for this OT, but I'm struggling with this problem. Do you know if it's possible to get amount of a mapped object key or, in other words, the amount of types in an union type ?

I want to check against an array of a certain length. The length is the number of keys of some mapped object but is dynamic (could be any property value of something like:

interface I {
foo: {a:number}
bar: {g: boolean}
}

These types are autogenerated by a tool from an API specification. The user defines the key when the function is called f('foo', ...) so it's unknown at compile time.

So far I'm able to validate members of the input array, but I would also like to validate it's length in some cases. So I need a way to get the number of keys in a mapped object or the number of types in an union type (since it seems object keys type are always represented with union types)

// I want to make sure arg0 is an array of length equals to the number of keys of I[T] (given)
function f(type: T, arg0: Tuple<T, UnionCount>)

type Tuple<TItem, TLength extends number> = [TItem, ...TItem[]] & { length: TLength };
type ValueOfStringKey<T extends { [k: string]: any }, K extends string> = T[K];

In that case, do you know if it's possible to implement the UnionCount helper ? You have the opposite (array to union) in your library so I though perhaps you have a clue.

Thanks in advanced, BTW your library is awesome. Thanks

@andnp
Copy link
Owner

andnp commented Feb 1, 2019

I must admit, there are some scary things going on in here. But I gave it a shot, we'll see if it is useful to you. I can't seem to find a way to send a union to an array or tuple, but there is an open discussion about it in the TS repo: microsoft/TypeScript#13298. The typescript team seems to be making headway on building up the tuple type, so I could see something like this making its way in at some point.

From what I gather, one other way to solve your problem would be to create a utility that can count the number of keys in an object? For instance:

interface Car {
  tires: number;
  doors: string;
  engine: object;
}

type numKeys = CountKeys<Car>; // 3

To do this, I relied on this stackoverflow question: https://stackoverflow.com/questions/50374908/transform-union-type-to-intersection-type

It came out to this:

interface I {
    hi: number;
    there: string;
    friend: string;
}

// takes an object, and returns its values in an intersection
type IntersectionOfValues<T> =
  { [K in keyof T]: (p: T[K]) => void   } extends
  { [n: string]:    (p: infer I) => void } 
    ? I 
    : never;

// example:
type a = IntersectionOfValues<I>; // => number & string & string
// which simplifies to // => number & string
 
// takes the first argument of each function in the intersection and puts it into the tuple
// order is not guaranteed, so this isn't a very useful type outside of this context
type IntersectionOfFunctionsToTuple<F> =
    F extends {
        (a: infer A): void;
        (b: infer B): void;
        (c: infer C): void;
    } ? [A, B, C] :
    F extends {
        (a: infer A): void;
        (b: infer B): void;
    } ? [A, B] :
    F extends {
        (a: infer A): void
    } ? [A] :
    never;

type ToTuple<T> =
    // pass the intersection of these functions to create the tuple type
    // store the keys as arguments to functions so that they can be retrieved with inference later
    IntersectionOfFunctionsToTuple<
        // convert each key into a function that takes that key type as an argument
        IntersectionOfValues<{ [K in keyof T]: (v: K) => void }>
    >;

type CountKeys<T> = ToTuple<T>['length'];

type numKeys = CountKeys<I>;

It is too bad that it would require adding more cases to IntersectionOfFunctionsToTuple to make this work, but maybe you'll only need a fixed, finite number of cases?

Hopefully, this helps! I'm glad simplytyped has been useful for you :)

@cancerberoSgx
Copy link
Author

Thank you very much! , yes It's not trouble to add more cases manually to IntersectionOfFunctionsToTuple, and I also feel that something is scary about this. But I really want to validate that, not only the values of an array argument are correct, but also its length. THanks again!!! keep it up !

@andnp
Copy link
Owner

andnp commented Feb 1, 2019

No problem! Really glad I was able to help. Hopefully, typescript will make it possible to come up with a more elegant solution to some of these with time. The language has been growing and improving so quickly, I imagine a better solution will be possible in the not too distant future.

@cancerberoSgx
Copy link
Author

I agree and although I'm not an expert on many strongly typed languages, I wonder if others allow API authors to be as declarative as you can with TypeScript! I also envision great future not only in this area but also on language service plugins, compiler API, transformations, etc. The future will be interesting... Thanks!

@fightingcat
Copy link

fightingcat commented Mar 8, 2019

// union to intersection of functions
type UnionToIoF<U> =
    (U extends any ? (k: (x: U) => void) => void : never) extends
    ((k: infer I) => void) ? I : never

// return last element from Union
type UnionPop<U> = UnionToIoF<U> extends { (a: infer A): void; } ? A : never;

// prepend an element to a tuple.
type Prepend<U, T extends any[]> =
    ((a: U, ...r: T) => void) extends (...r: infer R) => void ? R : never;

type UnionToTupleRecursively<Union, Result extends any[]> = {
    1: Result;
    0: UnionToTupleRecursively_<Union, UnionPop<Union>, Result>;
    // 0: UnionToTupleRecursively<Exclude<Union, UnionPop<Union>>, Prepend<UnionPop<Union>, Result>>
}[[Union] extends [never] ? 1 : 0];

type UnionToTupleRecursively_<Union, Element, Result extends any[]> =
    UnionToTupleRecursively<Exclude<Union, Element>, Prepend<Element, Result>>;

type UnionToTuple<U> = UnionToTupleRecursively<U, []>;

// support union size of 43 at most
type Union43 = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
    30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
    40 | 41 | 42 | 43;
type Tuple = UnionToTuple<Union43>;

@andnp I made some improvements so that it can work with union of any type, also made it work with larger union. BTW, in your original IntersectionOfFunctionsToTuple, more conditional types make no differences with single one, it always results empty interface if the intersection doesn't fit in, right now I just simply convert them into never we can retrieve just one element instead.
With this we can iterate properties now.
Update: simplified it.

@cancerberoSgx
Copy link
Author

cancerberoSgx commented Mar 17, 2019

@fightingcat that's awesome. Do you know if it's possible not to enforce the union order in the Tuples ?

@cancerberoSgx
Copy link
Author

cancerberoSgx commented Mar 17, 2019

@fightingcat How is that doesn't fail to compile since UnionToTupleRecursively and UnionToTupleRecursively_ depend on each other ? And BTW if I try to use your type to build an tuple but without enforcing the union order, then the compiler won't finish to compile - seems like it enters on your recursion (?) - tested with TypeScript 3.3.333 and 3.1.3, here is the code:

// union to intersection of functions
type UnionToIoF<U> =
  (U extends any ? (k: (x: U) => void) => void : never) extends
  ((k: infer I) => void) ? I : never

// return last element from Union
type UnionPop<U> = UnionToIoF<U> extends { (a: infer A): void; } ? A : never;

// prepend an element to a tuple.
type Prepend<U, T extends any[]> =
  ((a: U, ...r: T) => void) extends (...r: infer R) => void ? R : never;

type UnionToTupleRecursively<Union, Result extends any[]> = {
  1: Result;
  0: UnionToTupleRecursively_<Union, UnionPop<Union>, Result>;
  // 0: UnionToTupleRecursively<Exclude<Union, UnionPop<Union>>, Prepend<UnionPop<Union>, Result>>
}[[Union] extends [never] ? 1 : 0];

type UnionToTupleRecursively_<Union, Element, Result extends any[]> =
UnionToTupleRecursively<Exclude<Union, Element>, Prepend<Element, Result>>;

type UnionToTupleOrdered<U> = UnionToTupleRecursively<U, []>;

// /** return an fixed length array with item type TItem */
type Tuple<TItem, TLength extends number> = [TItem, ...TItem[]] & {
	length: TLength;
};

type UnionCount<T>=UnionToTupleOrdered<T>['length']
type UnionToTuple<T, L  extends number> = Tuple<T,  UnionCount<T>>
type UserTuple = UnionToTuple<1|2, 2>
var c : UserTuple = [1, 2]

@fightingcat
Copy link

@cancerberoSgx I'm guessing, it caused ts server into infinte recursion while checking constrain for Tuple<T, UnionCount<T>>, in the case T is not instantiated, don't know how typescript do the check, haven't read the code (have a shitty network to clone the repo).

Right now I have no idea how to fix it, I think it's a bug of type checker.

@cancerberoSgx
Copy link
Author

I think it's a TS issue failing to verify type declaration referencing itself in that particular case. And your solution is exploiting that issue. But just a guess.

@andnp BTW I'm also a collection of some type helpers (not as advanced as yours) and I wanted to test these, but instead using a compile-time check like you are doing (or tsd-check) does, I wanted to known if it's possible to do it at run time, by compiling the code at run time (using the same project configuration). This way I'm able to check compile error situations. For example:

describe('particular problem tsd-check-runtime is good at', ()=>{
  it('Tuple wont verify out of range access', ()=>{
    expect(`
      declare var a: Tuple<number, 2>
      var c = a[3] 
    `).toCompile()
  })
  it('ArrayLiteral will verify out of range access', ()=>{
    expect(`
      declare var a: ArrayLiteral<number, 2>
      var c = a[3] 
    `).not.toCompile()
  })
})

/* the types are:
export type Tuple<TItem, TLength extends number> = [TItem, ...TItem[]] & {
  length: TLength
}
export type ArrayLiteral<T, L> = 1 extends L ? [] :1 extends L ? [T] : 2 extends L ? [T, T] : 3 extends L ? [T, T, T] : 4 extends L ? [T, T, T, T]: 5 extends L ? [T, T, T, T, T] : 6 extends L ? [T, T, T, T, T, T]: 7 extends L ? [T, T, T, T, T, T, T]: 8 extends L ? [T, T, T, T, T, T, T, T]: 9 extends L ? [T, T, T, T, T, T, T, T, T]: never
*/

Wonder if you know a way of perform these kind of checking without having to compile at runtime ? Also wanted to share the tool with you since perhaps could be useful in this project.

The tool: https://github.com/cancerberoSgx/tsd-check-runtime
My poor man's type library: https://github.com/cancerberoSgx/misc-utils-of-mine/tree/master/misc-utils-of-mine-typescript

Would love your thoughts about this. Thanks!, keep it up

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants