TypeScript Exercises Bonus🦠 - Answers Part 1
Pragmatic Maciej

Pragmatic Maciej @macsikora

About: 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.

Location:
Lublin
Joined:
Mar 25, 2019

TypeScript Exercises Bonus🦠 - Answers Part 1

Publish Date: Mar 23 '20
10 9

For details of questions and requirements please visit the questions. This post will include only answers. If you are not aware of what this is about, then please take some time with the questions article. Also I would recommend the read about TypeScript type system as a language, which can help with understanding what we are doing here.

This post will include half of the answers as questions and the solution difficulty is significantly higher than previous questions in the series.

Answer 1

The question was: Make type level function which will check if two patients can meet. CanMeet should return or true or false depends if patients can or can't meet.

In order to achieve that we should use conditional type expression. This expression can be also nested in similar matter we use standard ternary operator.

type CanMeet<A extends Patient, B extends Patient> = 
  A extends Quarantine ? false // cannot meet with anybody
  : A extends Sick ? B extends Sick ? true : false // two sick people can meet
  : A extends Healthy ? B extends Healthy ? true : false // two healthy can meet
  : false // other combination cannot meet
Enter fullscreen mode Exit fullscreen mode

Full solution in the playground

Answer 2

The question was: Make type level function which will get all sick patients from the collection of patients. GetSick should filter the collection for only sick patients.

Note below solution is made in TS 4.0 with use of Variadic Tuple Types. It was rewritten as previous solution had problems with new versions of TS.

// utility types needed for adding/removing head of list
type Unshift<A, T extends unknown[]> = [A, ...T];
type Shift<T extends Array<any>> = T extends [unknown, ...infer Rest] ? Rest : T

// below direct solution 
// we have compiler error about circular dependency 🛑:
type GetSickNotWorking<
Patients extends Patient[]
, SickPatients extends Patient[] = []
> 
= Patients['length'] extends 0 
? SickPatients 
: 
(Patients[0] extends Sick 
? GetSickNotWorking<Shift<Patients>, Unshift<Patients[0], SickPatients>> 
: GetSickNotWorking<Shift<Patients>, SickPatients>);

// working solution with a mapped hack:
type GetSick<
Patients extends Patient[]
, SickPatients extends Patient[] = []
> 
= Patients['length'] extends 0 
? SickPatients 
: {
  [K in keyof Patients]: 
  Patients[0] extends Sick 
  ? GetSick<Shift<Patients>, Unshift<Patients[0], SickPatients>> 
  : GetSick<Shift<Patients>, SickPatients>
  }[0];

Enter fullscreen mode Exit fullscreen mode

The goal was filtering only sick patients from given collection of patients. This was achieved by utility types Shift and Unshift which allow on removing/adding elements from tuple types (tuple type is exactly our collection type at the type level).

Explanation

  • Second argument SickPatients 🤒 is kind of accumulator, remember reduce function? The reason of having it is exactly accumulating sick patients.
  • K in keyof Patients - its really hack in order to avoid circular dependency error
  • Patients['length'] extends 0 ? SickPatients - if our Patients list is already empty we end the computation
  • Patients[0] extends Sick ? GetSick<Shift<Patients>, Unshift<Patients[0], SickPatients>> : GetSick<Shift<Patients>, SickPatients> - if patient is sick we put it into SickPatients list by Unshift and remove it from Patients list. If Patient is not sick we remove it from Patients but without attaching it into SickPatients
  • [0] we get first element, this is part of the hack

Let's follow the algorithm for the example use case (its simplified view):

// patients:
type John = {name: 'John'} & Sick
type Tom = {name: 'Tom'} & Healty
type Kate = {name: 'Kate'} & Sick

type Check = GetSick<[John,Tom,Kate]>
Enter fullscreen mode Exit fullscreen mode

First iteration ➰:

  • Patients: [John,Tom, Kate]
  • SickPatients: []
  • We check if John is sick, he is
  • We remove John from Patients
  • We add John to the beginning of SickPatients // by Unshift
  • We call next iteration

Second iteration ➰:

  • Patients: [Tom, Kate]
  • SickPatients: [John]
  • We check if Tom is sick, he is not
  • We remove Tom from Patients
  • We call next iteration

Third iteration ➰:

  • Patients: [Kate]
  • SickPatients: [John]
  • We check if Kate is sick, she is
  • We remove Kate from Patients
  • We add Kate to the SickPatients

Fourth iteration ➰:

  • Patients list is empty
  • calculation returns SickPatients

The result is [Kate, John]. As you can see order is reversed as we are adding items in the beginning. But the goal is achieved, we get the sick patients 👌

The full solution is available in the playground

Additional challenge 🔥

There was additional/extended question to the second one - Can you make state of the patient as an argument? And make function which will get patients for given condition? Example usage would be Get<Patients, Healthy>. As we have now GetSick implemented, can you try to make it more flexible? Put your answer in the comment section (preferred playground link).

Try yourself with the rest of questions! 🔥

There are two questions more in The Bonus Questions. As you see the solution of the first two questions, maybe it will inspire you to make other two. Don't give up, check your skills 💪.

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

Comments 9 total

  • WindTraveler
    WindTravelerMay 25, 2020

    Well, I find out the answer2 seems not work.
    How can I fix it ?
    Thx in advanced.
    img

    • Pragmatic Maciej
      Pragmatic MaciejMay 25, 2020

      Interesting, TS can have impact. Can you share yours?

      • Pragmatic Maciej
        Pragmatic MaciejAug 17, 2020

        Ok the example solution was rewritten in TS 4.0 with using Variadic Tuple Types. Now works! Again thanks for the notice

  • dr-azbest
    dr-azbestMar 10, 2021

    Another aproach using UnionToTuple:

    type GetSick<Patients extends Patient[]> = UnionToTuple<{[P in keyof Patients] : Patients[P] extends Sick ? Patients[P] : never}[number]>
    
    
    Enter fullscreen mode Exit fullscreen mode

    Playground Link

  • Regev Brody
    Regev BrodyMar 31, 2021

    Another solution (that already includes the generic solution):

    type State = Sick | Healthy | Quarantine
    
    type Head<T extends Array<any>> = T extends [infer H, ...any[]] ? H : never
    
    type GetByState<Patients extends Patient[], S extends State, Acc extends Patient[] = []> = 
    Patients['length'] extends 0
      ? Acc
      : Head<Patients> extends S & Patient
          ? GetByState<Shift<Patients>, S, Unshift<Head<Patients>, Acc>>
          : GetByState<Shift<Patients>, S, Acc>
    
    type GetSick<Patients extends Patient[]> = GetByState<Patients, Sick>
    
    Enter fullscreen mode Exit fullscreen mode

    playground

  • Nenad Novaković
    Nenad NovakovićMay 12, 2021

    My solution to #1 is way simpler:

    type CanMeet<A extends Patient, B extends Patient> = A['state'] extends B['state'] 
      ? true 
      : false
    
    Enter fullscreen mode Exit fullscreen mode

    At least it passes all given conditions.
    I was unable to solve #2 and #3.

    • Pragmatic Maciej
      Pragmatic MaciejMay 13, 2021

      Ye you have nail that as the false is only when we have different states. That is very good answer 👍

      • ArthurKa
        ArthurKaSep 3, 2022

        I guess it's better not to bind the implementation to discriminated field value (state). It should remain variable.

  • wangfengming
    wangfengmingJun 10, 2021
    type Sick = { state: 'sick' }
    type Healthy = { state: 'healthy' }
    type Quarantine = { state: 'quarantaine' }
    type State = Sick | Healthy | Quarantine;
    type Patient = { name: string } & State;
    
    Enter fullscreen mode Exit fullscreen mode
    type CanMeet<A extends Patient, B extends Patient> =
      A extends Quarantine ? false :
        B extends Quarantine ? false :
          A['state'] extends B['state'] ? true :
            false;
    
    Enter fullscreen mode Exit fullscreen mode
    type GetByState<P extends any[], S extends State> =
      P extends [infer F, ...infer R]
        ? F extends S ? [F, ...GetByState<R, S>] : GetByState<R, S>
        : [];
    
    type GetSick<P extends Patient[]> = GetByState<P, Sick>;
    
    Enter fullscreen mode Exit fullscreen mode
    type Shift<T extends any[]> = T extends [any, ...infer R] ? R : [];
    type CanAccomodate<Beds extends '🛌'[], Patients extends Patient[]> =
      Patients['length'] extends 0 ? true :
        Beds['length'] extends 0 ? false :
          CanAccomodate<Shift<Beds>, Shift<Patients>>;
    
    Enter fullscreen mode Exit fullscreen mode
    type AddArray<T extends any[]> = {
      [K in keyof T]: [T[K]]
    };
    
    type Segragate<Patients extends Patient[]> = {
      sick: GetByState<Patients, Sick>;
      quarantine: AddArray<GetByState<Patients, Quarantine>>;
      healty: GetByState<Patients, Healthy>;
    }
    
    Enter fullscreen mode Exit fullscreen mode
Add comment