kolurbo
kolurbo

Reputation: 538

Construct partial object and infer types correctly

I have an object A and I want to create a new object B that contains some of the fields of object A. It's a pretty straightforward task however I still cannot figure out how to type it correctly. It's better to explain it with code:

interface Data {
    userName: string
    photoUrl: string
    age: number
}

const data: Data = {
    userName: 'John',
    photoUrl: 'https://...',
    age: 100
}


// new data can look like for example {userName: 'John'}
const newData: Partial<Data> = {}


// 1. solution - doesn't work because key is of type string
for (const key in data) {
    /* if (//some complex condition ) */
    newData[key] = data[key]
}


// 2. solution - keys are typed correctly but still doesn't work properly
const keys = Object.keys(data)  as Array<keyof typeof data>
keys.forEach(key => {
    /* if (//some complex condition ) */
    newData[key] = data[key]
})


// Anything else I am missing? :)

Playground here

Upvotes: 1

Views: 223

Answers (1)

Connor Low
Connor Low

Reputation: 7186

There is no built-in way to iterate over narrowly typed object keys (type keyof T rather than string) of an object. Here's a great article that explores some approaches to this TS problem.

A few solutions:

  1. Use Object.entries and assert the key's type:
for (const [str, value] of Object.entries(data)) {
    const key = str as keyof typeof data;
    //    ^^^ : keyof Data
    newData[key] = value;
    //             ^^^^^ : any
}
  1. Narrow the type of your key variable by declaring it outside the for statement:
// This one is similar to casting the result of `Object.keys`, but a bit less verbose.  
let key: keyof Data;
for (key in data) {
    newData[key] = data[key] as any;
}

Note: if your object has more than one property type, you have to cast the value to any to make it work on an assignment, or you will get an error like Type 'string | number' is not assignable to type 'undefined'.. In your example, given key with type keyof Data, TSC can infer that the value of data[key] is either a number or a string. However, it cannot infer which key is being referenced during a given iteration of for (key in data), which means it cannot determine if the value of data[key] is the same type as newData[key]. If age was a string (so that every property was a string type), it would work as expected.

For example:

interface I1 {
  a: string
  b: number
}

interface I2 {
  a: string
  b: string
}

const i1: I1 = { a: 'a', b: 0 }
let key: keyof I1;
for(key in i1) {
  i1[key] = i1[key]; 
//^^^^^^^ Error: is this a number or a string? And which do I need?
}

const i2: I2 = { a: 'a', b: 'b' }
let key2: keyof I2;
for(key2 in i1) {
  i2[key2] = i2[key2]; // Ok: I am getting a string and I need a string.
}

Playground

Upvotes: 1

Related Questions