Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Toxic optionals - TypeScript
András Tóth
András Tóth

Posted on

     

Toxic optionals - TypeScript

In my previous blog post I was talking about the inherentToxic flexibility of theJavaScript language itself.

I made a case for cutting down the number of options a piece of code can have so our tool chain including your IDE of choice can help you serving with just the right thing you need at the right moment, or help you "remember" every place a given object was used without having to guess it by use a "Find in all files" type dialog.

Howevertoxic flexibility can sprout up inTypeScript as well.

Let's start with a real life product example!

Building a survey

In our company we have to deal with surveys aka questionnaires. Overly simplified eachsurvey will have a number ofquestions of different types.

Let's say our product manager says:"I want people to have the ability to add aninteger or astring question."

For example:

  • How many batteries were present? =>integer question
  • How would you describe your experience? =>string question

Let's write the types down (I omit most of the details like IDs to keep it clean):

typeQuestion={answerType:'string'|'integer';label:string;}
Enter fullscreen modeExit fullscreen mode

The next day the product manager comes in and says:"I want these types to have constraints: astring question might haveminimum andmaximum lengths, whileinteger questions might haveminimum andmaximum values."

OK, we scratch our heads and then decide to go "smart" and say:"You know what? I will have just amin andmax property. The propertymin will mean if it isstring aminimum length and if it isinteger aminimum value."

typeQuestion={answerType:'string'|'integer';label:string;min:number;max:number;}
Enter fullscreen modeExit fullscreen mode

(Note: at this point we started to stray away fromtrue domain objects to make ourinitial implementation simpler. I will come back to this later.)

The next day the product manager comes in again:"All was well and good, but now I want aboolean question (a yes-no one), which does not have amin-max type of constraint. Also I wantmin-max values to be optional. Also people want to make photos and want to have a constraint over the maximum number of photos they can make but I do not wish to set a minimum."

So we go and update our type:

typeQuestion={answerType:'string'|'integer'|'yes-no'|'images';label:string;min?:number;max?:number;maxNumberOfPhotos?:number;}
Enter fullscreen modeExit fullscreen mode

Finally the product manager comes to tell:"Oh no, I completely forgot! We want people to have a question type where they select from a list of options with a radio button. I will call itsingle choice."

Now things start to sour:

typeQuestion={answerType:'string'|'integer'|'yes-no'|'image'|'single-choice';label:string;min?:number;max?:number;maxNumberOfPhotos?:number;choices?:string[];}
Enter fullscreen modeExit fullscreen mode

Looks like we can handle all these types with one excellenttype! Or is there a drawback...? 🤔

Cartesian products and the poison of optional properties

Let's see what kind of objects we can make from thisQuestion type:

// no surprisesconstvalidImage:Question={answerType:'image',maxNumberOfPhotos:3,};constvalidInteger:Question={answerType:'integer',min:1,max:10,};// but also this will compile...constinvalidYesNo:Question={answerType:'yes-no',maxNumberOfPhotos:13,choices:['lol','wat'],}
Enter fullscreen modeExit fullscreen mode

Whenever you use optional you create the Cartesian product of all possible missing and added properties! We have 4 optional properties now we will have 24 options: 16 possible types of which only 4 of them arevalid domain objects!

Look at how it all ends... up ⚠️

A several years in my coding career I got really aware that to write good code I should not just see my module (be it a class or a function or a component) on its own, I constantly need to check how it is used: is it easy or is it cumbersome to interact with the object I have just defined.

The type I created above will be extremely cumbersome to use:

// overly simplified logic just to show the problem// This is a simple React example, don't worry if you// are not familiar with itfunctionShowQuestion(question:Question){if(question.type==='yes-no'&&(question.max||question.min||question.maxNumberOfPhotos||question.choices)){thrownewError('Uh-oh, invalid yes-no question!');}if(question.type==='single-choice'&&(question.max||question.min||question.maxNumberOfPhotos)&&!question.choices){thrownewError('Uh-oh, invalid single-choice question!');}// and so on and so on - finally we can show itreturn<div>{question.max&&question.type==='integer'&&<Constraintlabel="Maximum value"value={question.max}/>}{question.maxNumberOfPhotos&&question.type==='image'&&<Constraintlabel="Maximum no of photos"value={question.maxNumberOfPhotos}/>}...</div>;}
Enter fullscreen modeExit fullscreen mode

Optional properties and distinct domain types are not going well together

Optional properties aretotally fine when you work with, say, customization options like styling: you only set what you wish to change from a sensible default.

However in this case we tried to describe several distinct domain types with just one type!

Just imagine if you only had one HTML tag and you would need to set tons of flags to achieve the same behaviorsdiv,p and other tags would do:

<!-- how a p tag would look like --><the-only-tagtype="paragraph"flow-content="yes"block="yes"types-that-cannot-be-children="ul, ol, li"> This would be a nightmare to work with as well!</the-only-tag>
Enter fullscreen modeExit fullscreen mode

Drill this into your forehead:

Everyoptional property you set up will warrant at least anif somewhere else.

If you need to describemultiple domain objects withonly one type you are most likely will need to use tons ofifs and duck typings...

Therefore in this particular use caseoptional became toxic.

Union type to the rescue!

I promised to come back to the domain objects. In everyone's mind we only have 5 types. Let's make then only five (plus a base)!

typeQuestionBase={answerType:'string'|'integer'|'yes-no'|'image'|'single-choice';label:string;}// I am not going to define all of them, they are simpletypeIntegerQuestion=QuestionBase&{// pay attention to this: answerType is now narrowed down// to only 'integer'!answerType:'integer';minValue?:number;maxValue?:number;}typeImageQuestion=QuestionBase&{answerType:'image';// we can make now things mandatory as well!// so if product says we must not handle// infinite number of photosmaxNumberOfPhotos:number;}// ...typeQuestion=IntegerQuestion|ImageQuestion;// | YesNoQuestion | ...
Enter fullscreen modeExit fullscreen mode

How do we use them? We are going to usenarrowing (see link for more details).

A case for someswitch-case

One of my favourite things to do when you have to deal with a stream of polymorphic objects is to useswitch-case:

functionrenderAllQuestions(questions:Question[]){questions.forEach(question=>renderOneQuestion(question));}functionrenderOneQuestion(question:Question){// question.type is valid on all question types// so this will workswitch(question.type){case'integer':renderIntegerQuestion(question);return;case'string':renderStringQuestion(question);return;//...}}// Check the type! We are now 100% sure// it is the right one.functionrenderIntegerQuestion(question:IntegerQuestion){// your IDE will bring `maxValue` up after you typed 'ma'console.log(question.maxValue);return<div>{question.maxValue&&<Constraintlabel="Maximum value"value={question.maxValue}/></div>}// ...
Enter fullscreen modeExit fullscreen mode

Disclaimer: I know there are nicer React patterns than having a render function for everything. Here I just wanted to make a kind of framework-agnostic example.

What happened above is that we were able tofunnel a set of types to concrete types without having to use the dangerousas operator or to feel out the type at hand with duck-typing.

Summary

To sum it all up:

  • optional properties result in conditions that check them leading toCartesian product explosion
  • we cut down the number ofinvalid possibilities to only5 validdomain objects
  • thesedomain objects alsomatch the terminology product management and clients have
  • since weencapsulated what is common inQuestionBase now we are free to add question specific extras and quirks
  • instead of having a god-component question handler that handles rendering of a question with an insane set of conditions (and growing!) we now boxed away the differences neatly inseparate, aptly-typed components
  • we can also handle anarray of different values and without any type casting with (e.g.question as IntegerQuestion) we created a type-safe system

Questions? Did I make errors?
Let me know in the comments.

Top comments(3)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
ashoutinthevoid profile image
Full Name
  • Education
    Rice
  • Joined

Nice post.

Quite the rollercoaster. When you started adding unrelated fields to the cronenberg type in your bad example, i immediately started saying "oh no...oh no...". The eventual good result was quite the relief after that stressful buildup.

CollapseExpand
 
captainyossarian profile image
yossarian
TypeScript blog catchts.com
  • Joined

Agree, that optional properties is toxic.
Personaly, I think it is better to use discriminated unions. One optional property creates several condition statements

CollapseExpand
 
latobibor profile image
András Tóth
A developer with M.Sc. in Computer Science. Working professionally since 2010. In my free time I make music and cook.Also I don't and after the recent events will not have Twitter.
  • Location
    Budapest
  • Education
    Eötvös Loránd University (ELTE - Budapest Hungary) Computer Science M. Sc.
  • Work
    Senior Full-stack and React-Native Engineer
  • Joined

It is also a case where usingtypescript and some goodtype narrowing clearly shines overvanilla js.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

A developer with M.Sc. in Computer Science. Working professionally since 2010. In my free time I make music and cook.Also I don't and after the recent events will not have Twitter.
  • Location
    Budapest
  • Education
    Eötvös Loránd University (ELTE - Budapest Hungary) Computer Science M. Sc.
  • Work
    Senior Full-stack and React-Native Engineer
  • Joined

More fromAndrás Tóth

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp