DEV Community

Cover image for TypeScript Exercises Bonus🦠 - Answers Part 1
Pragmatic Maciej
Pragmatic Maciej

Posted on • Edited on

TypeScript Exercises Bonus🦠 - Answers Part 1

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!

Top comments (9)

Collapse
 
dvlden profile image
Nenad Novaković

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.

Collapse
 
macsikora profile image
Pragmatic Maciej

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

Collapse
 
arthurka profile image
ArthurKa • Edited

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

Collapse
 
wangfengming profile image
wangfengming • Edited
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
Collapse
 
regevbr profile image
Regev Brody

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

Collapse
 
drazbest profile image
dr-azbest • Edited

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

Collapse
 
windtraveler profile image
WindTraveler • Edited

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

Collapse
 
macsikora profile image
Pragmatic Maciej

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

Collapse
 
macsikora profile image
Pragmatic Maciej

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