6
votes

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.

3
Is order important to you ? I don't think this is possible. - Titian Cernicova-Dragomir
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
Can you say more about your original problem? We might be able to find another solution. - Matt McCutchen
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
I hope @MattMcCutchen still cares to help. I've added the original problem to the answer. - Nurbol Alpysbayev

3 Answers

2
votes

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
votes

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

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