Skip to content

Instantly share code, notes, and snippets.

@DanielRosenwasser
Created March 16, 2018 07:46
Show Gist options
  • Save DanielRosenwasser/cddd875b53b987b8bb5ee3db56a0ad93 to your computer and use it in GitHub Desktop.
Save DanielRosenwasser/cddd875b53b987b8bb5ee3db56a0ad93 to your computer and use it in GitHub Desktop.
Conditional Types Conversation 2018-01-03

[[Early thoughts around conditional types]]

Conditional Types

  • Currently have T extends Foo as a type operator that returns true or false.

    • Seems very strange as to what that actually means:
      declare function foo<T>(x: T, y: T extends string): void;
    • This function takes an x, and if x is a string, y must be true or otherwise false.
    • Not entirely useful.
  • Key idea: you want to be able to narrow in the true branch of a conditional type.

    • Example
      type DeepReadonly<T> = T extends any[] ? ReadonlyArray<T[number]> :
                             T extends object ? DeepReadonly<T> :
                             T;
    • Need to be able to know that T is an array type to actually perform the indexed access T[number]
      • Could say users need to replace instances of T with T & any[]

        • Not correct though - (T & any[])[number] gives you any.
        • If we have type unknown = {} | null | undefined and used unknown[] might be better?
          • Eh, gives weird union types.
      • Really what you want is to refine the constraint of T.

      • Idea: T extends Foo, where extends is a type operator that augments the constraint of T.

        • Kind of like intersecting, more correct and consistent.
        • What happens when you stack these together? e.g. T extends Foo extends Bar?
          • Simplifies to T extends Foo & Bar
      • Users could potentially write these types out?

        • Sure, but not entirely useful; usually would only come out from narrowing in a conditional type.
      • Instantiation?

        • Instantiate LHS, if it's still generic, remove RHS.
      • What about narrowing in an expression context?

        function f<T>(x: T) {
            if (typeof x === "string") {
                return x;
            }
            throw new Error("Gimme string!");
        }
        • The return type here should be T extends string.
          • What about when T is number?

          • If you verify against the RHS at instantiation, you can get never!

          • Becomes a matter of...

            • declaration-site/call-site verification:
            //           vvvvvvvvvvvvvv
            function f<T extends string>(x: T) { /*...*/ }
            
            f(100); // nope!
            • vs use-site verification
            // T extends string -> number extends string -> never
            let x: never = f(100);
            x.whoops; // nope
        • In a sense, this is sort of related to type predicates!
          • Type predicate annotations guarantee a true value if a type matches some type.
          • This returns a value of a more specific type using information upon instantiation.
  • In general, we are trying to decide what is the appropriate behavior for the extends syntax.

    • One option is the "is-assignable" operator.
    • The other is the "type parameter constraint augmentation" operator
    • We want both in the lanugage.
    • Could make it context-depenent?
      • Could get rid of the "is-assignable" operator, make it part of conditional types.
        • Means it's harder to compose with And<T, U> and Or<T, U>.
        • But composing doesn't even keep track of the information that's flowed through. e.g.
          • And<T extends Foo, T extends Bar> ? /*trueBranch*/ : /*falseBranch*/
            • We don't have a way of tracking that T should be treated as T extends Foo & Bar in trueBranch.
      • In general, this might be confusing for users.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment