DEV Community

loading...

Advanced TypeScript Exercises - Answer 5

Pragmatic Maciej
I am Software Developer, currently interested in static type languages (TypeScript, Elm, ReScript) mostly in the frontend land, but working actively in Python also. I am available for mentoring.
Updated on ・2 min read

1. Conditional types solution

type Config = {
  name: boolean;
  lastname: boolean;
};
type User = {
  name?: string;
  lastname?: string;
};

declare function getUser<
  C extends Config,
  _NamePart = C['name'] extends true ? Pick<Required<User>, 'name'> : {},
  _LastNamePart = C['lastname'] extends true ? Pick<Required<User>, 'lastname'> : {}
  >(
    config: C
): _NamePart & _LastNamePart;

Enter fullscreen mode Exit fullscreen mode

Check the solution in the playground.

Explanation

  • declaration have generic type C extends Config in order to be able to work with narrowed variable type
  • we have created two local variables _NamePart and _LastNamePart, the purpose is readability
  • _NamePart = C['name'] extends true ? Pick<Required<User>, 'name'> : {} - if type variable C has name property set at true we assign type which has required field name from the original type User
  • _LastNamePart = C['lastname'] extends true ? Pick<Required<User>, 'lastname'> : {} - in the same way as before we ask about lastname
  • _NamePart & _LastNamePart we return intersection type, it means depends on what we get in particular parts we join those parts.

Take a look that I have used monoid property as in both conditional types I just fallback to {} as neutral element of & operation.

2. Overloads solution

type Config = {
  name: boolean;
  lastname: boolean;
};
type User = {
  name?: string;
  lastname?: string;
};

declare function getUser(
  config: { name: true; lastname: false}
): Pick<Required<User>,'name'>;

declare function getUser(
  config: { name: false; lastname: true}
): Pick<Required<User>,'lastname'>;

declare function getUser(
  config: { name: false; lastname: false}
): {};

declare function getUser(
  config: { name: true; lastname: true}
): Required<User>;
Enter fullscreen mode Exit fullscreen mode

Check the solution in the playground

The solution with overloads is less sophisticated but also longer, in order to achieve the result we need to create overload for every possible correlation of both fields in Config, so exactly 4 versions.

  • Pick<Required<User>,'name'> we pick only name field from Userand say its required.
  • Required<User> - we say we get User but with all fields non-optional

Utility types used in both solutions:

  • Pick - allows for picking only wanted set of properties
  • Required - produce a type with all properties required

Also want to mention more sophisticated solution by Rahul Kashyap left in the comment - Solution from the comments section. Good job Rahul!

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

Discussion (2)

Collapse
regevbr profile image
Regev Brody

Here is a generic solution that will support future interface changes:

declare function getUser<C extends Config>(
     config: C
): Required<Pick<User,{
    [k in keyof Config]: C[k] extends true ? k : never;
}[keyof Config]>>;
Enter fullscreen mode Exit fullscreen mode
Collapse
regevbr profile image
Regev Brody • Edited

Here is an even more generic solution to any type (not just User):

type Config<Type extends object> = {
  [key in keyof Type]-?: boolean;
};

type TrueKeys<Type extends object, Conf extends Config<Type>> = {
    [key in keyof Conf]: Conf[key] extends true ? key : never
}[keyof Type]

type Get<Type extends object> = 
  <Conf extends Config<Type>>(config: Conf) => 
    Required<Pick<Type, TrueKeys<Type, Conf>>>;

type User = {
  name?: string;
  lastname?: string;
  age: number;
};
type UserConfig = Config<User>

// Here declaration to be changed 🔥
declare const getUser: Get<User>

// test cases
const user = getUser({ name: true, lastname: false, age: false })
user.name.toLocaleLowerCase() // this field should be non-optional
user.lastname // this field should not be there and we should have compile error 🛑
user.age // this field should not be there and we should have compile error 🛑

const user2 = getUser({ name: true, lastname: true, age: true })
user2.name.toLocaleLowerCase() // this field should be non-optional
user2.lastname.toLocaleLowerCase() // this field should be non-optional
user2.age.toFixed() // this field should be non-optional

const user3 = getUser({ name: false, lastname: true, age: false })
user3.name // this field should not be there and we should have compile error 🛑
user3.lastname.toLocaleLowerCase() // this field should be non-optional
user3.age // this field should not be there and we should have compile error 🛑

const user4 = getUser({ name: false, lastname: false, age: true })
user4.name // this field should not be there and we should have compile error 🛑
user4.lastname // this field should not be there and we should have compile error 🛑
user4.age.toFixed() // this field should be non-optional

const user5 = getUser({ name: false, lastname: false, age: false })
user5 // user4 should be empty object {}
Enter fullscreen mode Exit fullscreen mode