Merging types in typescript (adding keys to an existing keyof type)

7.4k views Asked by At

If I have a typescript type consisting of keys:

const anObject = {value1: '1', value2: '2', value3: '3'}
type objectKeys = keyof typeof anObject

and then I wish to add keys to that type, while retaining the current keys, how do I go about doing that?
for example, if I wanted to add the keys 'get_value1', 'get_value2', 'get_value3' to the type 'objectKeys'

In the end, I want a type that looks like so:

type objectKeys = keyof anObject + 'get_value1', 'get_value2', 'get_value3'

without having to manually define the keys prefixed with 'get_', I understand that I can type out the keys to create this object - however that is not feasible for my use case. I simply want to add some keys that may or may not exist to the type 'objectKeys'

I am also aware that I can create a generic or any type that allows for any key value, however I must know the actual key names. It does not help me to allow for ANY key to be requested of the object, I need the existing keys + the ones I'd like to add.

Thanks for any help.

added for clarity:

const anObject = {val1: '1', val2: '2'}
type objectKeys = keyof typeof anObject

Object.keys(anObject).forEach(key => {
  const getAddition = `get_${key}`
  anObject[getAddition] = getAddition
})

// now I don't know whats next, how do I update objectKeys to include the 
// additions added in the forEach loop.

// What I really want is to not have to add the 'get' values to the object 
// at all, JUST to the type.  I want typechecking for the get values that 
// may or may not actually exist on the object.

hope thats clearerer and such.

3

There are 3 answers

1
jcalz On BEST ANSWER

It sounds like you're asking for concatenation of string literal types: that is, you want to be able to take the string literal "get_" and another string literal like "value1", and have TypeScript understand that if you concatenate strings of those types you get a string of the type "get_value1". Unfortunately, this feature does not exist as of TypeScript 2.4 (and probably won't exist in 2.5 or 2.6 either) .

So there's no way to do what you're asking for and maintain strict type safety. You can, of course, relax the type safety and allow access for any unknown key:

const anObject = {val1: '1', val2: '2'};
const openObject: { [k: string]: any } & typeof anObject = anObject;
// replace "any" above with whatever type the get_XXX values are

Object.keys(openObject).forEach(key => {
  const getAddition = `get_${key}`
  openObject[getAddition] = getAddition
})
openObject.val1 = 1; // error, val1 is known to be a string
openObject.get_val1 = 1; // no error, get_val1 is any
openObject.gut_val4 = 1; // no error, oops, sorry

but you said you don't want to do that.


In that case, the suggestion I'd make is to give up on adding arbitrary keys to the object, and instead make the getters (or whatever they are) hang off a single get property, like so:

const anObject = { val1: '1', val2: '2' }

type AnObject = typeof anObject;
type ObjectKeys = keyof AnObject;
type GetAugmentedObject = AnObject & { get: Record<ObjectKeys, any> }; 
// replace "any" above with whatever type the get.XXX values are 

const get = {} as GetAugmentedObject['get'];
Object.keys(anObject).forEach((key: ObjectKeys) => get[key] = key);
const augmentedObject: GetAugmentedObject = { ...anObject, get }

augmentedObject.val1; // ok
augmentedObject.val2; // ok 
augmentedObject.get.val1; // ok
augmentedObject.get.val2; // ok
augmentedObject.get.val3; // error, no val3
augmentedObject.git.val1; // error, no git

This is not very different for the developer (obj.get.val1 vs. obj.get_val1) but makes a big difference to TypeScript's ability to follow along. If you're in any control of the code that's adding the keys I strongly advise doing something TypeScript-friendly like this, since you don't want to spend your time fighting with TypeScript if you don't have to.


Otherwise, if only string concatenation at the type level will work for you, and you feel your use case is compelling enough, maybe you should go to the relevant GitHub issue and give it a and describe why it's a must-have for you.

Hope that helps. Good luck!

0
Richard Dunn On

You can use template literals.

Here's an example where the property id must be # + key:

interface MyInterface<K extends string> {
    something: {
        [key in K]: { id: `#${key}` }
    }
}

So the following is correct:

let x: MyInterface<'foo'> = {
    something: {
        foo: { id: '#foo' }
    }
}

But this is incorrect:

let y: MyInterface<'foo'> = {
    something: {
        foo: { id: 'foo' }
    }
}
0
Archit Garg On

2020 Update

It's there in typescript v4.1.0

You probably need this - https://github.com/microsoft/TypeScript/pull/40336