2

Think of the following line of code in Typescript:

let x: 'a' | 'b' extends 'a' ? true : false;

I was wondering that the type of x would be true since intuitively 'a' | 'b' is an extended version of 'a' (at least my intuition says so). I thought extends would work like subsets in math. A extends B iff B ⊆ A.

However, it seems that the actual type x is false here. I guess I don't understand how exactly the extends keyword works.

VIVID
  • 555
  • 6
  • 14
  • 1
    If `Y` extends `X` then you could substitute `Y` wherever an `X` is needed. Can you substitute `'b'` where `'a'` is needed? – Jeff Bowman Jan 02 '22 at 20:29
  • @JeffBowman So, do you mean my intuition (B ⊆ A) should actually go other direction (A ⊆ B) to say `A extends B`? – VIVID Jan 02 '22 at 20:33
  • The super class is sort of like a subset of the subclass (leaving aside abstract classes) - but a character two different characters of the same type can't be subsets of each other. – fredrik Jan 02 '22 at 20:36

1 Answers1

7

Per the Liskov Subsitution Principle, if "Y extends X" or equivalently "Y is a subtype of X", then a value conforming to type Y can be used wherever a value conforming to type X is requested. An subtype type is more specific, not more permissive. This leads to a counter-intuitive conclusion: if Y extends X, Y is more constrained than X. All Ys are Xs, but not all Xs are Ys.

In your example, because 'a' | 'b' could be either 'a' or 'b', that union type does not extend type 'a': 'b' wouldn't substitute for 'a'. Instead, 'a' extends ('a' | 'b'), because all values that match 'a' would work where 'a' | 'b' is requested.

As such, A extends B iff A ⊆ B.

One reason this is less intuitive in your TypeScript example is that it deals in unions of literal values. It might make more sense for us to think of this in terms of objects, where {foo: number, bar: number} extends {foo: number}. The latter supertype, {foo: number}, could have a bar property of any type or no bar at all. The former subtype {foo: number, bar: number} is more specific and more constrained: not only is foo a number, but bar is also a number. This also matches the use of extends in class or interface definitions: The subclass or subinterface adds properties and methods, which further constrains instances compared to the superclass or superinterface.

This explanation is consistent with the fact that never is assignable to everything: never is the most constrained type because no actual values match it, so never extends everything. The empty set is a subset of every set; the empty type never is the subtype of every type and can be assigned to every other type.

let unionExtendsMember: 'a' | 'b' extends 'a' ? true : false;
//  ^? let unionExtendsMember: false

let memberExtendsUnion: 'a' extends 'a' | 'b' ? true : false;
//  ^? let memberExtendsUnion: true

let objectExtendsObject: {foo: number, bar: number} extends {foo: number} ? true : false;
//  ^? let objectExtendsObject: true

let neverExtendsObject: never extends { foo: number } ? true : false;
//  ^? let neverExtendsObject: true

Playground Link

Jeff Bowman
  • 90,959
  • 16
  • 217
  • 251
  • 1
    Perfect answer. Thank you. – VIVID Jan 03 '22 at 17:01
  • Very nice and thoughtful answer! Perfect that you also brought up how objects behave with respect to `extends`. I was lacking this sort of intuition. – Andru Mar 23 '23 at 11:47
  • Thanks for the great answer! I still struggle to reconcile the idea of interface extension with the set theory notion of a subset. Given Interface X and Y, if Y extends X, this would be equivalent to Y ⊆ X in set notation. However, why is it that we can define extra properties on Y that are not found on the parent interface X? – PieterT2000 Aug 04 '23 at 10:55
  • @PieterT2000 Good question; I've thought about this too. You have to be clear whether the subset applies to the type or the value set it matches. The type/interface itself is a set of constraints: at a language level each method or field must exist with the right type and signature, and at a semantic level it must behave as specified. More constraints leads to fewer theoretical matching objects. The subtype Y has a _superset of constraints_ compared to X, which leads to a _subset of objects_ that match Y compared to what matches X. The latter is what I intended in phrasing this as set theory. – Jeff Bowman Aug 04 '23 at 15:22