3

I have two interfaces with identical optional keys, but different values:

interface Obj1 {
  a?: string
  b?: string
  c?: number 
}

interface Obj2 {
  a: boolean
  b: string
  c: number 
}

Obj1 is served as a function argument, the other, Obj2, is the return of that function. I want the return type to identify only the given keys on Obj1. So if Obj1 contained only a and b then Obj2 will contain only a and b as well.

I tried with the approach bellow, but I get a ts error Type 'Property' cannot be used to index type 'ValueType'

type Obj1KeysWithObj2Values<KeyType extends Obj1, ValueType extends Obj2> = {
  [Property in keyof KeyType]: ValueType[Property]
}

UPDATE: The function and its call

const myFunc = <T extends Obj1>({ a, b, c }: T) => {
  const returnObj: Partial<Obj2> = {}
  if (a) {
    returnObj.a = true
  }
  if (b) {
    returnObj.b = '1'
  }
  if (c) {
    returnObj.c = 20
  }
  return returnObj as Obj1KeysWithObj2Values<T, Obj2>
}

const resultObj = myFunc({ a: 'hello', b: 'hello' })

If you try it, then you see on resultObj you get what ever you pass to the function, as long as it is in interface Obj1, regardless of Obj2.

1
  • Would you update your question with the signature of the function you're trying to use this with? Commented Jan 10, 2023 at 12:46

4 Answers 4

3

Consider using Pick<Type, Keys> which is a part of standard library:

interface Obj1 {
 a?: string
 b?: string
 c?: string
}

interface Obj2 {
 a?: boolean
 b?: boolean
 c?: boolean
}

type Result = Pick<Obj2, keyof Obj1>

First argument represents source object, second argument represents a union of keys which should be picked

In your case, you also need to make an intersection of Obj1 keys and Obj2 keys:

interface Obj1 {
  a?: string
  b?: string
  c?: number
}

interface Obj2 {
  a: boolean
  b: string
  c: number
}

const myFunc = <T extends Obj1>({ a, b, c }: T) => {
  const returnObj: Partial<Obj2> = {}
  if (a) {
    returnObj.a = true
  }
  if (b) {
    returnObj.b = '1'
  }
  if (c) {
    returnObj.c = 20
  }
  return returnObj as Pick<Obj2, keyof T & keyof Obj2>
}

const resultObj = myFunc({ a: 'hello', b: 'hello' })

Playground

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

4 Comments

@captains-yossarian thank you for the reply. When I tried to use your approach as generic and to pass Obj1 dynamically it did not work, any idea?
@URogel Please provide reproducible example and explain what did you expect
@URogel - For what it's worth, I'd use typeof a !== "undefined" rather than a truthy test in your implementation (though of course it depends on what you're doing): tsplay.dev/mbKJbW
@captain-yossarianfromUkraine very slick solution! Thanks!
2

The issue is that although you know that Obj2 will always have a superset of Obj1's keys, TypeScript doesn't know that, so it needs reassuring. :-) If you want to use those generics, you can clear that error by using a conditional type to test that the property key is also present in ValueType:

type Obj1KeysWithObj2Values<KeyType extends Obj1, ValueType extends Obj2> = {
    [Property in keyof KeyType]: Property extends keyof ValueType ? ValueType[Property] : never;
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
};

Playground example

7 Comments

That said, there may be a simpler solution depending on how the function is actually using the types. If you can add the signature of the function to the question, I (or someone else) may be able to suggest something simpler.
I am not sure I understand the question, but maybe this solution helps: Pick<Obj2, keyof Obj1>
@captain-yossarianfromUkraine - I'm an eejit. :-D That's dramatically better than the above, I can't believe I didn't think of Pick. I suggest posting it, because it does answer the question as it stands now. I suspect there's some detail missing that may take the question another way, but that's true for the answer above as well; we can only work with what's in front of us.
happens with me all the time. Some times I publish extremely complicated recursive solution and somebody just puts oneliner :D
I suggest to to make key index like this: [K in keyof KeyType as K extends keyof ValueType ? K : never] so you will only have props matched between two types.
|
0

This answer is completely generic and it works.

I just want to share something similar which I did to convert Date to a custom DateTimeFormatParts formatted date using Intl, except it's partially generic.

It worked like this:

// Accepts T which has any or all Intl.DateTimeFormatOptions properties
export type DateFormatPartsRecord<T extends Intl.DateTimeFormatOptions = Intl.DateTimeFormatOptions> = {
  // modify keys to match props with result props
  [P in keyof T as P extends "fractionalSecondDigits"
    ? "fractionalSecond"
    : P extends "hour12"
    ? "dayPeriod"
    : P]?: string;
};

// argument type for my caller function
export type DateIntlOptions<T extends Intl.DateTimeFormatOptions = Intl.DateTimeFormatOptions> = {
  locale?: string | string[];
  options?: T;
};

// the function which converts formatted intl array parts to my custom object type.
export function ToDateFormatParts<T extends Intl.DateTimeFormatOptions = Intl.DateTimeFormatOptions>(
  val: number | string | Date,
  intl?: DateIntlOptions<T>
): DateFormatPartsRecord<T> {
  const toFormat = new Date(val);
  const reduced: DateFormatPartsRecord<T> = {};
  Intl.DateTimeFormat(intl?.locale, intl?.options ?? { day: "2-digit", month: "short", year: "numeric" })
    .formatToParts(toFormat)
    .forEach(({ type, value }) => {
      if (type === "literal") return;
      reduced[type] = value;
    });
  return reduced;
}

Applying this to your case, you can create a custom type based on the original:

// instead of obj2 interface:
type Obj2FromObj1<T extends Obj1> = {
  [K extends keyof T]: boolean;
} // or a shorter version: Record<keyof T, boolean>

So now you can create a function to return only what props defined in the obj1:

function Obj2ValuesFromObj1<T extends Obj1>(args: T): Obj2FromObj1<T> {
  // your handling here.
}

Comments

0

This is not my code, though it is still a valuable answer. It was posted by another user and shortly after the answer was removed so I don't know to whom to give the credit.

type Obj1KeysWithObj2Values<KeyType extends Obj1, ValueType extends Obj2> = {
  [Property in keyof KeyType]: Property extends keyof ValueType ? ValueType[Property] : never;
};

My explanation to it, though feel free to improve it, Obj1KeysWithObj2Values is generic type which accepts two params:

  1. KeyType which extends the function param type.
  2. ValueType which extends the function return type.

Then it is declaring a new type which is an object with keys as the keys in the KeyType and the values types, if they can be found on the ValueType will inherit the type from there or will be never.

2 Comments

It was mine, I've undeleted it if you found it helpful. But why do you prefer this over Pick?
@T.J.Crowder your solution worked, the Pick solution needed some modification to fit my use case. Thanks, I learned something useful none the less!

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.