Dusan Jovanov
Dusan Jovanov

Reputation: 567

Typescript - Return object with same keys as argument object

The return object needs to have only the keys that the argument object has and the keys have to be strings.

This is what I have so far:

type ArgumentObject<Key extends string> = Record<Key, string>;

type ResultObject<Key extends string> = Record<Key, boolean>;

function someFunction<Key extends string>(arg: ArgumentObject<Key>) {

  const result: ResultObject<keyof typeof arg> = {} as ResultObject<Key>;

  for (const k in Object.keys(arg)) {
    result[k as Key] = true;
  }

  return result;
}

Typescript correctly infers the shape of the result object only if the keys of the argument object are strings. But you can also put number and symbol as keys in the argument object.

const result = someFunction({
  a: "abc",
  b: "def",
  1: "number allowed as key",
  [Symbol("a")]: "symbol also"
})

result.a // intellisense doesn't work

/////////////////////////////////////

const result = someFunction({
  a: "abc",
  b: "def"
})

result.a // intellisense works

Also, it would be great to not have to do all the type casting with as.

Upvotes: 1

Views: 743

Answers (1)

jsejcksn
jsejcksn

Reputation: 33929

First, there might be a misconception: when you provide a numeric index as a property when defining an object literal, it is coerced to a string. So in your example, 1 is a string, not a number:

{
  a: "abc",
  b: "def",
  1: "number allowed as key",
  [Symbol("a")]: "symbol also"
}

TypeScript is structurally-typed, so as long as a type can extend (be a subtype of) a required type, the compiler will allow it. In your case, the object passes the requirement of having string keys with string values (it also happens to have a symbol key).

One way to handle this is to use a conditional return type, where in the valid branch, it is a mapped type. So, while you can still pass in objects that have undesirable properties, you won't be able to use the return value if you do.

TS Playground

function someFunction <T extends Record<PropertyKey, string>>(arg: T): symbol extends keyof T ? never : number extends keyof T ? never : {
  [K in keyof T]: boolean;
} {
  const result = {} as any;
  for (const k in Object.keys(arg)) result[k as keyof T] = true;
  return result;
}

const result1 = someFunction({
  a: "abc",
  b: "def",
  1: "number allowed as key",
  [Symbol("a")]: "symbol also",
});

result1; // never
result1.a; /*
        ^
Property 'a' does not exist on type 'never'.(2339) */

const result2 = someFunction({
  a: 42, /*
  ^
Type 'number' is not assignable to type 'string'.(2322) */
  b: "def"
});

result2; // never
result2.a; /*
        ^
Property 'a' does not exist on type 'never'.(2339) */

const result3 = someFunction({
  a: "abc",
  b: "def"
});

result3; // { a: boolean; b: boolean; }
result3.a; // boolean

Upvotes: 1

Related Questions