DEV Community

Cover image for Advanced TypeScript Exercises - Question 8
Pragmatic Maciej
Pragmatic Maciej

Posted on

Advanced TypeScript Exercises - Question 8

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 👌
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (5)

Collapse
 
wangfengming profile image
wangfengming
type StringKeys<T extends object> = {
  [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

const concatToField =
  <T extends Record<string, any>, K extends StringKeys<T>>(obj: T, key: K, payload: string): T => {
    const prop = obj[key] as unknown as string; // compile error should not be here
    return { ...obj, [key]: prop.concat(payload) }; // compile error should not be here
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dwjohnston profile image
David Johnston • Edited

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
Pragmatic Maciej • Edited

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 • Edited

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 • Edited

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) };
}

?