loading...
Cover image for Advanced TypeScript Exercises - Question 8

Advanced TypeScript Exercises - Question 8

macsikora profile image Maciej Sikora ใƒป2 min read

Welcome back! Let's go back on track after bonus questions. This question will be less abstract, and more practical, we will land more at value level ๐Ÿš.

We have a function concatToField which takes object, and key of this object, and string value. The purpose of this function is to create a new object with concatenated property object[key] with third argument.

The question - How to type generic types T and K in the definition of concatToField function in order to achieve compile time guarantee that obj[key] can be only string.

const concatToField =
  <T /* here your code ๐Ÿ’ช*/, K /* here your code ๐Ÿ’ช*/>(obj: T, key: K, payload: string): T => {
    const prop = obj[key]; // compile error should not be here
    return { ...obj, [key]: prop.concat(payload) }; // compile error should not be here
}
// tests
const test = { fieldStr: 'text', fieldNum: 1, fieldStr2: 'text' };
concatToField(test, 'fieldStr', 'test'); // should be ok ๐Ÿ‘Œ
concatToField(test, 'fieldNum', 'test'); // should be error fieldNum is not string field ๐Ÿ›‘
concatToField(test, 'notExistingField', 'test'); // should be error - no such field ๐Ÿ›‘
concatToField(test, 'fieldStr2', 'test'); // should be ok ๐Ÿ‘Œ

Full code available in the playground

Important - body of the function should remain unchanged, no type assertion (as), or any changes of the body are allowed. The only thing needs to be done is constraint on T and K generic types.

Post your answers in comments (preferred links to the playground). Have fun! Answer will be published soon!

This series will continue. If you want to know about new exciting questions from advanced TypeScript please follow me on dev.to and twitter.

Discussion

pic
Editor guide
Collapse
dwjohnston profile image
David Johnston

Cool challenge.

const concatToField =
    <T extends Record<string, any>, K extends keyof T & T[K] extends string ? string : never> (obj: T, key: K, payload: string): T => {
    const prop = obj[key]; // compile error should not be here
    return { ...obj, [key]: prop.concat(payload) }; // compile error should not be here
}
// tests
const test = { fieldStr: 'text', fieldNum: 1, fieldStr2: 'text' };
concatToField(test, 'fieldStr', 'test'); // should be ok ๐Ÿ‘Œ
concatToField(test, 'fieldNum', 'test'); // should be error fieldNum is not string field ๐Ÿ›‘
concatToField(test, 'notExistingField', 'test'); // should be error - no such field ๐Ÿ›‘
concatToField(test, 'fieldStr2', 'test'); // should be ok ๐Ÿ‘Œ

Explanation:

T extends Record<string, any>

Pretty straight forward. The test object is a map of string keys to any values, though of course, if those values are strings, then we want to be able to apply the concatToField method to it.

 K extends keyof T & T[K] extends string ? string : never

I'll break this into two parts

K extends keyof T

By itself, this is just saying 'the value of K is going to be one of the keys of that T object'.

& T[K] extends string ? string : never

We're saying that 'is the value at T[K] of type string? If so, then the type of K is string, if not, then the type of K is never'.

The never type allows us to return compile errors when something is illogical/not allowed - in this case it is not allowed to to have a T[K] value that is not of type string.

Note:

It would be nice if we could just do

 K extends keyof T & T[K] extends string

instead of using that ternary - but this apparently is not valid typescript.

Collapse
macsikora profile image
Maciej Sikora Author

Hi David thank you for the answer.
But your code doesn't make errors when it should, so there are no compile time guarantees. I put in the snippet two places where the error should occur, you see there ๐Ÿ›‘ icon. Even more the code you have provided compiles for any second argument like concatToField(test, 'anything', 'test');.

So try again! And good luck.

Collapse
dwjohnston profile image
David Johnston

Ah, was just missing some brackets. :/

const concatToField =
  <T extends Record<string, any>,
    K extends keyof T & (T[K] extends string ? string : never)>(obj: T, key: K, payload: string): T => {
    const prop = obj[key]; // compile error should not be here
    return { ...obj, [key]: prop.concat(payload) }; // compile error should not be here
}

Playground

Collapse
jfet97 profile image
Andrea Simone Costa

What about:

const concatToField =
  <K extends keyof T, T extends {[key in K]: string} >(obj: T, key: K, payload: string): T => {
    const prop = obj[key];
    return { ...obj, [key]: prop.concat(payload) };
}

?