Skip to content
webtype.orgwebtype.org

    ↑↓ move · ⏎ open · esc close

    Play

    Extends

    A type on the left, a type on the right, one keypress: does it extend? 586 cards, and the compiler has already answered every one of them.

    Arcade · 586 cards

    Which branch does the compiler take?

    Every card is A extends B ? true : false. Answer before the fuse burns down. Five in a row lifts the multiplier; a miss costs one of three lives and shows you exactly what the compiler said.

    The same deal for everyone today. The first run is the one that counts.

    T←trueF→falseEscpause⏎start

    Rank
    @ts-nocheck
    10 right to @ts-check
    Daily streak
    0
    Cards known
    0/586

    Scores, ranks and streaks live in this browser and nowhere else.

    01How it is played

    One question, asked 586 ways

    1. 01

      Read the pair

      Two types and the word between them. Some cards come with a declaration to read first — an interface, an enum, a class.

    2. 02

      Take a branch

      true or false: T or ←, F or →, a tap, or a swipe of the card toward the side you mean.

    3. 03

      Beat the fuse

      Each card burns down. A right answer scores 100 a tier, plus up to half again for speed. A miss or a timeout costs a life.

    4. 04

      Hold the streak

      Every five in a row adds to the multiplier, up to ×8. Fifteen in a row wins back a lost life.

    02The deck

    36 rules of assignability

    Every card belongs to one rule, and every rule has cards on both sides of it — the pairs that pass and the pairs that do not. The squares are yours: a card you last got right, one you last missed, one you have not met yet.

    01

    Literals and primitives

    A literal type is one value of its primitive, so it extends that primitive — never the reverse, and never across primitives: "42" is a string, not a number. A number literal is its value however it is spelled: 0x10 is 16.

    • "a" extends string
      true
    • string extends "a"
      TS2322
    Your record: 0 of 30

    Lesson: Conditional types

    02

    Into a union

    A union on the right is a choice: whatever extends one of its members extends the whole union. PropertyKey is string | number | symbol.

    • "a" extends "a" | "b"
      true
    • "c" extends "a" | "b"
      TS2322
    Your record: 0 of 22

    Reference: Extract

    03

    Out of a union

    A union on the left has to fit as a whole: every member must extend the right side. boolean is nothing but true | false.

    • "a" | "b" extends string
      true
    • "a" | "b" extends "a"
      TS2322
    04

    null and undefined

    Under strictNullChecks, null and undefined are types of their own. They fit only where they are named — or unknown, any, and, for undefined alone, void.

    • undefined extends unknown
      true
    • null extends string
      TS2322
    05

    The top: unknown

    Every type extends unknown. unknown itself extends only what admits every value: unknown, any — or {} | null | undefined, which is the same set spelled out.

    • string extends unknown
      true
    • unknown extends string
      TS2322
    Your record: 0 of 16

    Error: TS18046

    06

    The bottom: never

    never is the type with no values, so it extends everything — and nothing extends it but never itself.

    • never extends string
      true
    • string extends never
      TS2322
    07

    any switches the check off

    Wherever any stands, that part of the comparison passes, in either direction. It does not change the structure around it, though: a missing property or a wrong tuple length still fails.

    • string extends any
      true
    • Record<string, any> extends { a: number }
      TS2741
    Your record: 0 of 15

    tsconfig: noImplicitAny

    08

    {} is not an empty object

    {} means “anything but null or undefined”, so primitives extend it; object shuts them out. Yet {} extends object while string extends {} — assignability is not transitive.

    • { a: string } extends {}
      true
    • null extends {}
      TS2322
    Your record: 0 of 20
    09

    String is not string

    The capitalised names are interfaces for the wrapper objects. A primitive extends its wrapper, and any object type built from the wrapper’s members as the wrapper types them — a string’s length is a number, even for "abc". A wrapper never extends the primitive, and Object behaves like {}.

    • string extends String
      true
    • String extends string
      TS2322
    Your record: 0 of 17
    10

    Extra properties are fine

    Object types are open. A type with more properties extends one with fewer — Error is just an interface with a name and a message. What fails is a required property that is missing.

    • { a: string; b: number } extends { a: string }
      true
    • { a: string } extends { a: string; b: number }
      TS2741
    Your record: 0 of 22

    Error: TS2741Error: TS2739

    11

    Property by property

    Properties are compared one at a time, in the same direction as the whole: a narrower property type fits, a wider one does not.

    • { a: "x" } extends { a: string }
      true
    • { a: string } extends { a: "x" }
      TS2322
    Your record: 0 of 16

    Error: TS2322

    12

    Optional is not | undefined

    An optional property may be missing altogether; a: T | undefined has to be there. So required fits optional, and optional does not fit required.

    • { a: string } extends { a?: string }
      true
    • { a?: string } extends { a: string }
      TS2322
    13

    The weak type check

    A type whose properties are all optional is “weak”. Anything that has properties but shares none of them is rejected with TS2559 — even though, structurally, it would fit.

    • { a: string; b: number } extends { a?: string }
      true
    • { b: number } extends { a?: string }
      TS2559
    14

    readonly properties do not count

    readonly on a property limits what you may do through that type, and assignability ignores it: { readonly a: string } and { a: string } extend each other.

    • { a: string } extends { readonly a: string }
      true
    • { readonly a: number } extends { a: string }
      TS2322
    15

    Arrays are covariant

    An array of narrower elements extends an array of wider ones. That is unsound for writes — push a number into a string[] seen as unknown[] — and TypeScript allows it anyway.

    • string[] extends unknown[]
      true
    • string[] extends "a"[]
      TS2322
    Your record: 0 of 16
    16

    Readonly arrays stay readonly

    A readonly array or tuple has no mutating methods, so it cannot fill a mutable slot (TS4104). A mutable array fits a readonly slot without complaint.

    • string[] extends readonly string[]
      true
    • readonly string[] extends string[]
      TS4104
    17

    Tuples have a length

    A tuple is an array whose length is part of its type — [1, 2] has length: 2. Tuples extend arrays of their elements; an array never extends a tuple, because it might have any length.

    • [string, number] extends (string | number)[]
      true
    • (string | number)[] extends [string, number]
      TS2322
    18

    Parameters run backwards

    Under strictFunctionTypes a function extends another only if it accepts everything the other may be passed: parameter types are compared in reverse, and an optional parameter may be passed undefined. Accepting more is the safe direction.

    • (x: string) => void extends (x: "a") => void
      true
    • (x: "a") => void extends (x: string) => void
      TS2322
    19

    Fewer parameters is fine

    JavaScript lets a function ignore its arguments, so one that takes fewer parameters extends one that takes more. Requiring an extra argument is what fails; a rest parameter counts as any number.

    • () => void extends (x: string) => void
      true
    • (x: string) => void extends () => void
      TS2322
    20

    Returns run forwards

    Return types are compared in the same direction as the functions: a function returning something narrower extends one returning something wider.

    • () => "a" extends () => string
      true
    • () => string extends () => "a"
      TS2322
    Your record: 0 of 18

    Reference: ReturnType

    21

    void means “ignored”

    A function type returning void promises only that nobody reads the result, so a function returning anything extends it. void itself is neither undefined nor null: only void, unknown and any accept it.

    • () => number extends () => void
      true
    • () => void extends () => number
      TS2322
    Your record: 0 of 13

    Error: TS2355

    22

    Methods are bivariant

    Method syntax opts out of strictFunctionTypes: a method’s parameters are compared in both directions, and it is the target’s syntax that decides. A function-typed property gets the strict check. It is why Set<"a"> extends Set<string>.

    • { m(x: string): void } extends { m(x: "a"): void }
      true
    • { m(x: number): void } extends { m(x: string): void }
      TS2322
    23

    Function, generics and constructors

    Every function extends Function, but Function promises no particular signature. A generic function extends each instantiation its constraint allows, not the reverse — and an abstract constructor cannot fill a slot that needs new.

    • () => void extends Function
      true
    • Function extends () => void
      TS2322
    24

    Overloads

    A type with several call signatures extends a single signature if any one of its overloads fits it. The other way round, one signature has to fit every overload of the target, one at a time.

    • { (x: string): string; (x: number): number } extends (x: string) => string
      true
    • (x: string) => string extends { (x: string): string; (x: number): number }
      TS2322
    Your record: 0 of 5

    Error: TS2769

    25

    Index signatures

    An index signature requires every property to fit it. An object type literal gets one implicitly; an interface does not, and neither does object — so neither extends Record<string, unknown>, while the same shape as a type alias does. A signature typed any is the exception: it takes any object.

    • { a: "x" } extends { [k: string]: string }
      true
    • { a: string; b: number } extends Record<string, string>
      TS2322
    Your record: 0 of 17

    Reference: RecordError: TS7053

    26

    Template literal types

    A template literal type is a pattern over strings. ${string} matches any text, the empty string included; ${number} any non-empty string JavaScript reads as a finite number — "1e3" and "0x1F" yes, "Infinity" no — and ${bigint} only whole ones. Lowercase<string> and its kin match text already in that case.

    • "id-42" extends `id-${number}`
      true
    • "id-x" extends `id-${number}`
      TS2322
    27

    Intersections

    An intersection has every member’s properties, so it extends each member. One that no value can inhabit — string & number, two different kind literals — reduces to never, and never extends everything.

    • { a: string } & { b: number } extends { a: string }
      true
    • { a: string } extends { a: string } & { b: number }
      TS2322
    28

    Unions of objects

    An object extends a union of objects when it fits one of the members. Since TypeScript 3.5 an object whose discriminant is itself a union is split into one object per value, and each piece is checked — so { kind: "a" | "b" } extends { kind: "a" } | { kind: "b" }.

    • { kind: "a"; a: 1 } extends { kind: "a"; a: number } | { kind: "b" }
      true
    • { kind: "c" } extends { kind: "a" } | { kind: "b" }
      TS2322
    29

    keyof

    keyof is the union of a type’s known keys: numeric keys stay numbers, an array’s include every method name, and a string index signature’s are string | number. keyof {} and keyof unknown are never; keyof never is every key there is.

    • keyof { a: 1; b: 2 } extends "a" | "b"
      true
    • "a" | "b" | "c" extends keyof { a: 1; b: 2 }
      TS2322
    30

    Indexed access

    T[K] is the type of property K of T, and a union of keys gives a union of types. On an array, T[number] is the element type; on a tuple, the union of its elements, and T["length"] its literal length. An optional property brings its undefined with it.

    • { a: string }["a"] extends string
      true
    • { a: string }["a"] extends number
      TS2322
    33

    Classes are structural — until private

    Two classes with the same public shape extend each other, and a plain object type can extend a class; statics live on the constructor, so they do not count. A private, protected or #private member makes a class nominal: only its own declaration and its subclasses have that member.

    • class Dog { name = "" } class Cat { name = "" }Cat extends Dog
      true
    • class A { x = 1 } class B extends A { y = 2 }A extends B
      TS2741
    Your record: 0 of 12

    Pattern: Brand a primitive

    34

    Enums

    A numeric enum still accepts any number, and any literal that is one of its values — but not an out-of-range literal. Each member extends its own value, so S.A extends "a"; yet a string enum accepts only its own members, and "a" is not S.A.

    • enum E { A, B }E.A extends number
      true
    • enum E { A, B }E extends E.A
      TS2322
    Your record: 0 of 15

    tsconfig: erasableSyntaxOnly

    35

    unique symbol

    A unique symbol is a literal of symbol: it extends symbol and PropertyKey, symbol does not extend it, and no two of them extend each other. The well-known symbols, like Symbol.iterator, are unique symbols too.

    • typeof Symbol.iterator extends symbol
      true
    • symbol extends typeof Symbol.iterator
      TS2322
    Your record: 0 of 6

    Reference: typeof

    36

    Generics and variance

    A generic type is compared through its type argument, in the direction the type uses it: properties and return types covariantly — mutable properties too — and parameters of function types contravariantly. in and out state the direction outright, overriding what the compiler would measure; in out pins it invariant.

    • Promise<"a"> extends Promise<string>
      true
    • Promise<string> extends Promise<"a">
      TS2322

    03Verified

    The compiler dealt every card

    Each card is compiled at build time with TypeScript 6.0.3, in strict mode, twice: as the conditional type it shows, which must resolve to exactly the verdict on the card, and as an assignment, which must fail when the verdict is false and pass when it is true. The build stops if any card disagrees. The message you read after a miss is quoted from that run, never written by hand.

    Cards
    586
    Extend
    340
    Do not — each with its diagnostic
    246
    Diagnostic codes
    6
    TS2322 · TS2559 · TS2739 · TS2740 · TS2741 · TS4104

    Scores, ranks and streaks live in this browser and nowhere else.