Typescript: object type to array type (tuple)
Asked Answered
R

4

14

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.

Rae answered 17/10, 2018 at 12:43 Comment(11)
Is order important to you ? I don't think this is possible.Amyloid
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 :-(Rae
Can you say more about your original problem? We might be able to find another solution.Deason
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?Anne
I hope @MattMcCutchen still cares to help. I've added the original problem to the answer.Rae
@SeanSobey I guess I want the latter, you can see my updated answer.Rae
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.Deason
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.Rae
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.Rae
@NurbolAlpysbayev Sorry, I somehow missed your comments. I was proposing paramsDeclaration = [{name: 'param1', value: REQUIRED<string>(), shape: ...}, {name: 'param2', ...}]. It's a little clunker than your current syntax, but the names are still clearly visible.Deason
The type system of TS is Turing complete (I just read a blog and didn't understand it deeply). [Here](typescriptlang.org/play?ts=4.0.2#code/…Beacham
A
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
Anne answered 18/10, 2018 at 15:40 Comment(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.Rae
B
6

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

Botzow answered 7/8, 2021 at 19:22 Comment(2)
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}?Resee
@YihaoGao I don't believe there is any way to programmatically set the name of a tuple element.Botzow
A
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
Anne answered 18/10, 2018 at 15:40 Comment(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.Rae
A
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
Allisonallissa answered 19/3, 2020 at 11:30 Comment(0)
O
1

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'; }; }
]
Orola answered 22/3, 2023 at 23:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.