← Back to context

Comment by robertlagrant

7 hours ago

This feels right, and I also have never done it (or had the guts to get others to do it).

The reason I've not is - say there's an optional field. Currently we call that null, probably, and check each time if it's there or not. I could instead make a type, like User and UserWithPhoneNumber. Should we be making types for each combination of present/absent fields? That can't be right.

The classic answer is to move the logic inside the domain object, or have a helper function outside the object, so you aren't constantly checking for field presence/absence, but are instead writing the logic once and calling some code.

I'm not sure in practice types can help with this. But I'd love to be proven wrong.

I think this is a slightly different problem. The absence of an optional field, if that's a legal state, is meaningful every time you use the type, so you encode it on the field: `phone: ValidPhoneNumber | null`. When it's not null you're still guaranteed a valid phone number. When it is null, that's a legal state you have to handle and which is domain logic, not validation you forgot to do.

The combinatorial explosion you're picturing only shows up if you make a separate type per combination of present fields, but you don't need to. An independent optional field stays one `T | null`. You only reach for distinct types when fields are correlated and present together because they represent a state, and then it's a discriminated union on a status field, which is N states, not 2^N.

  • That's fair enough - I see what you mean. I think I read the case I was thinking into the article. Now I re-read it, it is saying what you're saying, which does make a lot of sense.

    Using types like this also means you can more easily avoid assignment errors, as everything will have a very specific type (e.g. Age instead of int).

This explosion of optionality types is (the most important) topic of Rich Hickey's "Maybe Not" talk. I recommend it!

The short version is: the shape of a type is inherent to the type itself, but the optionality of its members is dependent on the situation. A type system that solves this problem separates these concepts to allow for this distinction.

I _suspect_ it's possible to implement something like that in typescript but I haven't tried it myself (and I doubt it's very ergonomic).

if a user with/without phone number are equally valid states to be then types won't help you much. I think it's more about writing

  class User{phone: ?PhoneNumber}

over

  class User{phone: ?string}.

  • To expand and give some notion of good taste:

    It's more about writing

        struct User {phone: MaybePhoneNumber} // give or take, it's a monoid
    

    over

        struct User {phone: Option<String>}

    • I don't mind discussing syntax when appropriate, but this feels like arguing over which trivial brainfuck substitution[1] is the best.

      > monoid

      nullables with `??` and `?.` are also give-or-take monoids. is it common though to `or` two MaybePhoneNumbers together or to apply a PhoneNumber->MaybePhoneNumber function to it? if not then why mention it?

      let's see something meaningfully different like a database schema.

      [1] https://esolangs.org/wiki/Trivial_brainfuck_substitution