13

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.

Nurbol Alpysbayev
  • 19,522
  • 3
  • 54
  • 89
  • Is order important to you ? I don't think this is possible. – Titian Cernicova-Dragomir Oct 17 '18 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 :-( – Nurbol Alpysbayev Oct 17 '18 at 12:46
  • 2
    Can you say more about your original problem? We might be able to find another solution. – Matt McCutchen Oct 17 '18 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? – Sean Sobey Oct 17 '18 at 14:17
  • I hope @MattMcCutchen still cares to help. I've added the original problem to the answer. – Nurbol Alpysbayev Oct 17 '18 at 14:30
  • @SeanSobey I guess I want the latter, you can see my updated answer. – Nurbol Alpysbayev Oct 17 '18 at 14:31
  • Can you just structure the `paramsDeclaration` as an array instead of an object? If you want names for the parameters, you can put them in fields of the sub-objects. – Matt McCutchen Oct 17 '18 at 14:53
  • Do you mean the keys of `paramsDeclaration` should be 0, 1 instead of param1, param2 ? I am considering this at the moment, its a plan B. – Nurbol Alpysbayev Oct 17 '18 at 14:54
  • 0, 1 keys are less prefereable, because the very purpose of `paramsDeclaration` is in a declarative, readable form to display params, their names, and their other different traits, and having string keys is more readable/meaningful etc. – Nurbol Alpysbayev Oct 17 '18 at 15:03
  • @NurbolAlpysbayev Sorry, I somehow missed your comments. I was proposing `paramsDeclaration = [{name: 'param1', value: REQUIRED(), shape: ...}, {name: 'param2', ...}]`. It's a little clunker than your current syntax, but the names are still clearly visible. – Matt McCutchen Oct 19 '18 at 14:18
  • The type system of TS is Turing complete (I just read a blog and didn't understand it deeply). [Here](https://www.typescriptlang.org/play?ts=4.0.2#code/PTAEhsPQ0f0QA9AA5QqOUDFyht%20MGxKoAuAnAlgUwFgAoDATwAc9QAhHAOwEMtSAVXKgXlAG1e-eADABpQARgC6I7gCYRAZnGSe3ACwiArEu4A2EQHZFW3gA4RATi2jhYiUdGyxCqaLVjNhqX1G6xB56bELI2lraQkpaQdpBUVPGVdpTQifaQNggOkLKTlrOVsPZT45BzknHjlXOXdPOR85P3KAuSCjVWsVcJ4VBxUYzxVXFSSunxU02MLVAJUsnnVrdVtPdQd1Mu51V3V3VvUfdQaNgPUgz21rbU6dB20Yw1a%20bVdtYZ0fbTSzgO1Z7j1rPT5OJ6Bx6dZ6Vx6arKPQ%20PSHPQBPQtB7GazGK7GBzGPrKYyuYyvYw%20YzjOLGALGX5maxmJbKMwOMzrMyuMw7Cb8bhmHxmQ5mAJmU7KKzWKxXKwOKx3OJW – justTryIt Aug 07 '21 at 19:03

4 Answers4

5

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

kaya3
  • 47,440
  • 4
  • 68
  • 97
  • 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}`? – Yihao Gao Sep 26 '22 at 02:16
  • @YihaoGao I don't believe there is any way to programmatically set the name of a tuple element. – kaya3 Sep 26 '22 at 02:22
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
Sean Sobey
  • 942
  • 7
  • 13
  • 1
    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. – Nurbol Alpysbayev Oct 19 '18 at 07:12
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
rua.kr
  • 86
  • 1
  • 5
0

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'; }; }
]
dubble
  • 89
  • 1
  • 5