TypeScript's keyof and Structural Typing

As I explore the possible solution space for designing a type for the Pyret programming language with Ben Greenman, I’ve spent some time grappling with the concept of structural type compatibility. Specifically, I am interested in TypeScript’s type system and the keyof operator. This blog post focuses on summarizing what I’ve learned thus far (DISCLAIMER: this post might contain inaccuracies as I continue to refine my understanding).

If these concepts interest you, stay tuned. I plan to continue exploring these ideas and ultimately aim to answer questions like: can we leverage this concept within Pyret?

Type compatibility

We’ll use the term type compatible throughout this post, so let’s take a moment to formally define it. As best summed up by Nominative and Structural Typing, type systems must be able to answer two type compatibility questions:

  1. equivalence: given two type expressions T1 and T2, are T1 and T2 equivalent? In other words, are all objects of type T2 valid objects of type T1, and vice versa? Commonly written as T1 == T2
  2. subtype: given two type expressions T1 and T2, is T1 a subtype of T2? In other words, are all objects of type T1 also objects of type T2? Commonly written as T1 <= T2

Generally, there are two main approaches to answering these questions: structural typing and nominal typing.

Structurally typed: type compatibility is determined by the structure of the types

Nominally typed: type compatibility is determined by the name of the types

Consider the following example.

foo {
    bar: () => string
}

baz {
    bar: () => string
}

In a nominally typed language, foo != bar, however in a structurally typed language, foo == bar.

What about Duck Typing? Duck typing is dynamic type compatibility at runtime; both structural and nominative typing are static. Furthermore, typically at runtime we just care if an object has a specific property more so than if an object is really some type (i.e. don’t care as much about the type’s name or entire type structure).

Which is better? Of course, it depends: one of the main pros of nominal typing is differentiation between two types of the same structure, i.e. MeasurementInFeet and MeasurementInMeters. Structural typing is advantageous for its flexibility.

While we’re comparing structural and nominal type systems, note that hybrid systems (I don’t believe this is a technical term) exist.

At a base level, C++ is nominatively typed–two classes with different names are not considered equivalent; and subtyping must be explicitly declared (via inheritance). However, the template system uses structural typing–any type can be an argument to any template; and the compiler won’t complain unless a particular instantiation of a template is incompatible with its declaration. – Nominal and Structural Typing

Here is a nice table summarizing type compatibility for a large set of programming languages: Comparison of programming languages by type system - Wikipedia.


Structural Typing

I have much more experience with nominally typed systems: C#, Python, Java, Rust (albeit a bit fuzzy), Haskell (which is mostly nominal), etc. Thus, I will focus here on structurally typed languages.

TypeScript

TypeScript is a structurally typed programming language that transpiles to Javascript allowing for engineers to gradually introduce types to an existing, dynamic, duck typed, Javascript codebase and/or leverage the expansive JavaScript ecosystem with an additional layer of safety.

Unless you’re already familiar with TypeScript’s type system, I’d recommend opening up a TS Playground online editor in another screen as you read along.

In a structurally typed language, since we care about structure, not about name, an analogous operator for .class or .type is keyof (hand wavy, hand wavy), where keyof returns a concatenated collection of object properties.

type Point = { x: number; y: number };

// "x" | "y"
type P = keyof Point;

TypeScript does have the typeof operator, however it is limited (see more on limitations here).

type Point = { x: number; y: number };

// typeof pt = { x: number; y: number }
const pt: Point = { x: 10, y: 11 }

keyof Examples

So in what ways is keyof useful? It may not be immediately obvious… it certainly wasn’t for me!

interface Food {
    isTasty: boolean;
}

let pizza = { isTasty: true }

function isMyAThing(food: Food, attribute: keyof Food): boolean {
    return food[attribute];
}

isMyAThing(pizza, "isTasty")
isMyAThing(pizza, "isHealthy") // will fail

Here isMyAThing does two interesting things: (a) indexes properties of a type (b) constrains input of attribute to just being valid properties of Food.

type Boolify<T> = {
    // read "for each key in T translate to boolean
    [K in keyof T]: boolean
} 

interface SomethingUseful {
    name: string;
    age: number;
}

type SomethingLessUseful = Boolify<SomethingUseful>;

let notHelpful: SomethingLessUseful = { name: true, age: false };

Here Boolify maps all properties of the generic T to booleans,

Consider Record: which constructs an object type whose property keys are Keys and whose property values are Type. This utility can be used to map the properties of a type to another type. More formally: Record<Keys, Type> (Record Type - TypeScript Handbook)

type Qualities = {
    isTasty: boolean;
    isHealthy: boolean;
    calories: number;
}

interface Info {
    value: string
}

type Food = Record<keyof Qualities, Info>;

const asparagus: Food = {
    isTasty: { value: "sometimes" },
    isHealthy: { value: "yes!" },
    calories: { value: "idk" }
}
type Food = {
    isTasty: boolean;
}

type Decoration = {
    isPretty: boolean;
}

// intersection type: combination of all properties
type DecorativeFood = Food & Decoration;

const weddingCake: DecorativeFood = {
    isTasty: true,
    isPretty: true,
}

// union type: one type _or_ the other
type DecorationOrFood = Food | Decoration;

const bacon: DecorationOrFood = {
    isTasty: true
};
const painting: DecorationOrFood =  {
    isPretty: true
}

// union type and keyof
type DecorationOrFoodDescription = keyof Food | keyof Decoration;

const bacon: DecorationOrFoodDescription = 'isTasty'
const painting: DecorationOrFoodDescription =  'isPretty'

Conclusion

With a basic understanding of keyof and structural typing, the next question is can we implement a similar keyof operator for Pyret?

Pyret’s core is written in JavaScript. Thus, we should be able to, more-or-less, translate keyof from TypeScript to JavaScript and implement it as a built-in method for Pyret. However this assumes that core types are implemented as objects in a fairly consistent way… if not, implementing keyof might be a heavier lift than expected.

We’ll explore this in a following post.


References