1

I'm struggling to type a function with this behaviour : given a object conf with an undefined number of keys, each key being an object with value and type properties, the function should return an object with the same properties and only value as value.

So to be clear, this is an example of function input:

{
  foo: {value: 1, type: 'number'},
  bar: {value: 'hello', type: 'string'}
}

and the corresponding output:

{
  foo: 1,
  bar: 'hello'
}

Here is what I've written so far:

type TypeWithName<T extends string> = 
  T extends 'number' ? number :
  T extends 'boolean' ? boolean : 
  T extends 'undefined' ? undefined :
  T extends 'array' ? string[] :
  string

declare function f<T extends string>(conf: {
  [key: string]: { default: TypeWithName<T>; type: T }
}): { [P in keyof typeof conf]: TypeWithName<T> }

That is obviously not correct since :

  1. T can only take one type at a time (the example above will throw an error on property bar)
  2. The return type has an undefined number of keys, instead of being all and only keys present in input object.

But I am a little lost here and don't really know where to look, nor if that's even possible.

1
  • Do you need f() to care about the input value properties having the right relationship between their value and type properties? If so, what strings can type be? It looks almost like the runtime output of typeof value except that there's no "array" primitive (it would be object at runtime). Commented Sep 5, 2019 at 17:59

3 Answers 3

1

My approach would be to use a mapping interface from type property string to value type instead of a conditional type; you might be able to use a conditional type here but it's easier to work with interfaces in the type system:

interface TypeMap {
  number: number;
  boolean: boolean;
  undefined: undefined;
  string: string;
  object: object | null;
}

I assumed you wanted "string" to map to string, and I added "object". If you want "array" to map to string[] you can do that, I guess.

Then I want to use TypeMap to make a union type called Config representing the constraint between value and type, like this:

type Config = {
  [K in keyof TypeMap]: { value: TypeMap[K]; type: K }
}[keyof TypeMap];

If you inspect that, it is equivalent to:

type Config = {
    value: number;
    type: "number";
} | {
    value: boolean;
    type: "boolean";
} | {
    value: undefined;
    type: "undefined";
} | {
    value: string;
    type: "string";
} | {
    value: object | null;
    type: "object";
}

Now the f function is generic in T, an object type whose keys are whatever and whose properties are Config, represented by the constraint Record<keyof T, Config>. The input type is T, and the output type maps each property in T to its value property:

declare function f<T extends Record<keyof T, Config>>(
  t: T
): { [K in keyof T]: T[K]["value"] };

Let's see if it works:

const output = f({
  foo: { value: 1, type: "number" },
  bar: { value: "hello", type: "string" }
}); 
// const output: { foo: number;  bar: string; }

I think that's the output type you're expecting, right? If you need a narrower type (where e.g., foo is of type 1 instead of number), you can use a const assertion in the input:

const outputNarrower = f({
  foo: { value: 1, type: "number" },
  bar: { value: "hello", type: "string" }
} as const); 
// const outputNarrower: { foo: 1; bar: "hello"; }

Finally, you can see that the input to f() is constrained so that you can't give a mismatching value/type pair:

f({
  chicken: { value: 123, type: "string" }, // error!
  cow: { value: "abc", type: "string" }
});

Okay, hope that helps; good luck!

Link to code

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

1 Comment

Okay, that's very smart and well detailed, thank a lot ! I learn a few things thanks to you. value is optional in my real case application, but I can work from there and definitively find a complete solution. Again, thank you sir !
1

You could try an approach along these lines:

const conf = {
  foo: { value: 1, type: "number" },
  bar: { value: "hello", type: "string" }
};

type Input = {
  [key: string]: { value: any; type: string };
};

type Output<U extends Input> = { [Key in keyof U]: U[Key]["value"] };

function f<T extends Input>(input: T): Output<T> {
  // Details...
}

const returned = f(conf);

returned.foo // number
returned.bar // string

TypeScript playground

3 Comments

That a nice solution, but it constraint the type of Output[key] to the type of Input[key][value]. What I'm trying to do is constraining the type of Output[key] to the Input[key][type] with the help of my TypeWithName type.
Ahh, got it. I misunderstood. Do you need a type key if it can be inferred?
Yeah, I simplified the problem here for asking the question, but in my case the value will be optional and used only as a default one.
0

I think this should be enough..... key is using 'any'

function change(x:any) {
  let y:any={};
  for(let c in x){
    console.log(c)
    y[c] = x[c].value;
  }
  return y;
}

let x={
  'foo': {value: 1, type: 'number'},
  'bar': {value: 'hello', type: 'string'}
};

console.log(change(x));

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.