17

I have this:

interface Obj {
    foo: string,
    bar: number,
    baz: boolean
}

The desired type is this tuple:

[string, number, boolean]

How can I convert the interface to the tuple?

Update:

My original problem is: I make some opinionated library with a declarative spirit, where a user should describe parameters of a function in an object literal. Like this:

let paramsDeclaration = {
  param1: {
    value: REQUIRED<string>(),
    shape: (v) => typeof v === 'string' && v.length < 10
  },
  param2: {
    value: OPTIONAL<number>(),
    ...
  },
}

Then the library takes this object and creates a function with parameters from it:

   (param1: string, param2?: number) => ...

So, making such function is not a problem, the problem is to correctly type it, so that user gets good code-completion (IntelliSense).

P.S. I know it's not solvable, but it would be interesting to know what is the closest possible workaround/hack.

11
  • Is order important to you ? I don't think this is possible. Commented Oct 17, 2018 at 12:45
  • Hi again! Order is not important, however it would be better to maintain it. If you say its impossible, than it must be impossible indeed. Sad :-( Commented Oct 17, 2018 at 12:46
  • 2
    Can you say more about your original problem? We might be able to find another solution. Commented Oct 17, 2018 at 12:57
  • When you say 'convert' do you mean you want to create an array matching the tuple type from an object matching the interface? Or do you mean you want to change the Obj interface to be the tuple type? Commented Oct 17, 2018 at 14:17
  • I hope @MattMcCutchen still cares to help. I've added the original problem to the answer. Commented Oct 17, 2018 at 14:30

5 Answers 5

11

90% of the time you think something is impossible in Typescript, the real answer is that it is possible but you probably shouldn't do it.

Here's a solution using TuplifyUnion from this answer, which converts a union type into a tuple type; note that we need to start from a union of the object's keys, not its values, because the values may themselves be unions (e.g. boolean is technically true | false).

Read that linked answer for an elaboration of what the // oh boy don't do this comment means ─ and note the DISCLAIMER: DON'T DO THIS!!. If you want users of your API to specify the parameters of a function which your API generates, then the sane choice is to accept those parameter specifications in an array in the first place.

type ObjValueTuple<T, KS extends any[] = TuplifyUnion<keyof T>, R extends any[] = []> =
  KS extends [infer K, ...infer KT]
  ? ObjValueTuple<T, KT, [...R, T[K & keyof T]]>
  : R

// type Test = [string, number, boolean]
type Test = ObjValueTuple<Obj>

Playground Link

Sign up to request clarification or add additional context in comments.

4 Comments

Any way we can make the generated tuple named? Like [a: string, b: number, c: boolean] for the Obj of { a: string, b: number, c: boolean}?
@YihaoGao I don't believe there is any way to programmatically set the name of a tuple element.
Note that it is possible to generate all possible orderings of the tuple to avoid some of the problems with this approach. However, that scales very badly as the size of the union grows (there's a combinatorial explosion).
Maintaining a consistent order is truly impossible. Seemingly unrelated code can change this order. As this playground link demonstrates.
3

Not really an answer to the question, but since I don't actually think its possible to do, hopefully this is at least helpful in some way:

function REQUIRED<T>(): T {
    //...
}
function OPTIONAL<T>(): T {
    //...
}

interface ParamsDeclaration {
    readonly [paramName: string]: {
        readonly value: any;
        readonly shape?: Function;
    };
}

type Func<T> = T extends {
    readonly [paramName: string]: {
        readonly value: infer U;
    };
} ? (...params: Array<U>) => void
    : never;

function create<T extends ParamsDeclaration>(paramsDeclaration: T): Func<T> {

    // ...
}

const paramsDeclaration = {
    param1: {
        value: REQUIRED<string>(),
        shape: (v: any) => typeof v === 'string' && v.length < 10
    },
    param2: {
        value: OPTIONAL<number>(),
        //...
    },
};
// Type is '(...params: (string | number)[]) => void'
const func1 = create(paramsDeclaration);
func1('1', 2); // Ok
func1(2, '1'); // Ok, but I assume not what you want
func1(Symbol()); // TS error

1 Comment

Thank you for your time! It was interesting to discover your approach. Yes, unfortunately the order is important, but it seems there is no way to somehow infer order of properties of interface/object.
2

Alternate suggestions,
It needs to set orders of parameters.

interface Param {
    readonly value: any;
    readonly shape?: Function;
}
type Func<T extends Record<string, Param>, orders extends (keyof T)[]> = (...args:{
    [key in keyof orders]:orders[key] extends keyof T ? T[orders[key]]['value']: orders[key];
})=>void;

function create<T extends Record<string, Param>, ORDERS extends (keyof T)[]>(params: T, ...orders:ORDERS): Func<T, ORDERS> {
    return 0 as any;
}

const func1 = create({a:{value:0}, b:{value:''}, c:{value:true}}, 'a', 'b', 'c');
func1(0, '1', true); // ok
func1(true, 0, '1'); // error

or
ParamDeclarations with array

type Func2<T extends Param[]> = (...args:{
    [key in keyof T]:T[key] extends Param ? T[key]['value'] : T[key]
})=>void;

function create2<T extends Param[], ORDERS extends (keyof T)[]>(...params: T): Func2<T> {
    return 0 as any;
}

const func2 = create2({value:0}, {value:''}, {value:true});
func2(0, '1', true); // ok
func2(true, 0, '1'); // error

Comments

2

Seems like the consensus is its not possible atm, but I still wanted to try. Order cannot be guaranteed because objects do not preserve key order.

solution

export type Partition<T> = UnionToTuple<
  { [K in keyof T]: { [k in K]: T[K] } }[keyof T]
>

helpers

type Pack<T> = T extends any ? (arg: T) => void : never
type Unpack<T> = [T] extends [(arg: infer I) => void] ? I : never
type Into<T> = Unpack<Unpack<Pack<Pack<T>>>>

type UnionToTuple<T> = Into<T> extends infer U
  ? Exclude<T, U> extends never
    ? [T]
    : [...UnionToTuple<Exclude<T, U>>, U]
  : never

example

type Data = { a0: 'foo'; b0: { b1: 'bar' } }

type Mock = Partition<Data>
[
  { a0: 'foo'; },
  { b0: { b1: 'bar'; }; }
]

Comments

0

I wanted to do this so I can have a singular definition for request types my worker has to handle. But the result is so ugly I just stuck to having a duplicate function and object definitions.

Anyway, while others are correct in saying that objects aren't ordered, tuples _are_. If you encode the original definition as a tuple of pairs you can turn those into both objects:

type DefinitionToObject<T extends [PropertyKey, any][]> = {
  [I in keyof T as T[I] extends [infer K extends PropertyKey, any] ? K : never]:
    T[I] extends [any, infer V] ? V : never;
};

, and tuples:

type DefinitionToTuple<Pairs extends [string, any][]> = {
    [I in keyof Pairs]: Pairs[I] extends [any, infer T] ? T : never;
};

So you can have the cake and eat it too:

type Definition = [["filename", string], ["source", string], ["andnumber", number]];

type AsTuple = DefinitionToTuple<Definition>; // [ string, string, number ]
type AsObject = DefinitionToObject<Definition>; // { filename: string, source: string, andnumber: number }

But that's only the case if you control Definition and can change it to be this tuple of pairs structure. If you want to do so with some third party type you'll just have to duplicate it because objects aren't ordered.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.