TIL: Determining output types from input

TypeScript lacks (useful) function overloading. This makes sense — TypeScript largely has zero runtime representation. Function overloading is generally done by having multiple functions with the same name and different type signature. For example, in Java:

void doThing(int a) {
    System.out.println("Got an int!");
}

void doThing(String b) {
    System.out.println("Got a string!");
}

void main {
    doThing(1); // Got an int!
    doThing("1"); // Got a string!
}

The correct function will be called depending on the type passed in.

TypeScript is largely erased at runtime, so there isn’t a way to determine which function implementation should be called. For example, consider this hypothetical syntax:

function function doThing(a: number): voiddoThing(a: numbera: number) {
  var console: Consoleconsole.Console.log(...data: any[]): void (+2 overloads)
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
("Got an number!");
} function function doThing(a: number): voiddoThing(b: stringb: string) { var console: Consoleconsole.Console.log(...data: any[]): void (+2 overloads)
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
("Got a string!");
} function doThing(a: number): voiddoThing(1); function doThing(a: number): voiddoThing("1");

At runtime, do we want to call doThing(number) or doThing(string)?

JavaScript is weakly-typed, so it can’t know if you’re wanting it to type coerce or not. In this example a straightforward solution would be to call the strongest match, but it becomes more complicated when you consider more complex types. For example:

type 
type Person = {
    name: string;
    age: number;
}
Person
= {
name: stringname: string; age: numberage: number; }; type
type Dog = {
    name: string;
    weight: string;
}
Dog
= {
name: stringname: string; weight: stringweight: string; }; function function doThing(a: Person): voiddoThing(a: Persona:
type Person = {
    name: string;
    age: number;
}
Person
) {
var console: Consoleconsole.Console.log(...data: any[]): void (+2 overloads)
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
("Got a person!");
} function function doThing(a: Person): voiddoThing(b: Dogb:
type Dog = {
    name: string;
    weight: string;
}
Dog
) {
var console: Consoleconsole.Console.log(...data: any[]): void (+2 overloads)
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
("Got a dog!");
} function doThing(a: Person): voiddoThing({ name: stringname: "John", age: numberage: 21, weight: numberweight: 140, });

There aren’t a reasonable set of rules to determine which function to call. So, what can we do instead?

First, it’s worth mentioning that TypeScript does kinda have function overloads. I haven’t had a good experience using them, so I never use them.

Instead, I usually reach for conditional types. This lets me do something that kinda looks like function overloading. It allows me to write one function that handles multiple cases, and can even allow me to determine the output type based on the input.

For example:

type 
type BoardGame = {
    name: string;
    numberOfPieces: number;
}
BoardGame
= {
name: stringname: string; numberOfPieces: numbernumberOfPieces: number; }; type
type VideoGame = {
    name: string;
    platform: "PC" | "Console";
}
VideoGame
= {
name: stringname: string; platform: "PC" | "Console"platform: "PC" | "Console"; }; type type Game = BoardGame | VideoGameGame =
type BoardGame = {
    name: string;
    numberOfPieces: number;
}
BoardGame
|
type VideoGame = {
    name: string;
    platform: "PC" | "Console";
}
VideoGame
;
type type BoardGamePieces<G extends Game> = G extends BoardGame ? number : undefinedBoardGamePieces<function (type parameter) G in type BoardGamePieces<G extends Game>G extends type Game = BoardGame | VideoGameGame> = function (type parameter) G in type BoardGamePieces<G extends Game>G extends
type BoardGame = {
    name: string;
    numberOfPieces: number;
}
BoardGame
? number : undefined;
function function getNumberOfPieces<G extends Game>(game: G): BoardGamePieces<G>getNumberOfPieces<function (type parameter) G in getNumberOfPieces<G extends Game>(game: G): BoardGamePieces<G>G extends type Game = BoardGame | VideoGameGame>(game: G extends Gamegame: function (type parameter) G in getNumberOfPieces<G extends Game>(game: G): BoardGamePieces<G>G): type BoardGamePieces<G extends Game> = G extends BoardGame ? number : undefinedBoardGamePieces<function (type parameter) G in getNumberOfPieces<G extends Game>(game: G): BoardGamePieces<G>G> { if ("numberOfPieces" in game: G extends Gamegame) { return game: BoardGamegame.numberOfPieces: numbernumberOfPieces as type BoardGamePieces<G extends Game> = G extends BoardGame ? number : undefinedBoardGamePieces<function (type parameter) G in getNumberOfPieces<G extends Game>(game: G): BoardGamePieces<G>G>; } return var undefinedundefined as type BoardGamePieces<G extends Game> = G extends BoardGame ? number : undefinedBoardGamePieces<function (type parameter) G in getNumberOfPieces<G extends Game>(game: G): BoardGamePieces<G>G>; } const const result1: numberresult1 =
function getNumberOfPieces<{
    name: string;
    numberOfPieces: number;
}>(game: {
    name: string;
    numberOfPieces: number;
}): number
getNumberOfPieces
({
name: stringname: "Catan", numberOfPieces: numbernumberOfPieces: 100, }); const const result2: undefinedresult2 =
function getNumberOfPieces<{
    name: string;
    platform: "PC";
}>(game: {
    name: string;
    platform: "PC";
}): undefined
getNumberOfPieces
({
name: stringname: "Minecraft", platform: "PC"platform: "PC", });

Recent posts from blogs that I like

Perhaps not Boring Technology after all

via Simon Willison

Jerusalem Delivered: 7 The death of Clorinda

Tancred loses Erminia's trail in a wood, and is tricked into stepping into a dungeon. Battle rages outside Jerusalem, and Godfrey is forced to try to take the city, putting Clorinda at risk.

via The Eclectic Light Company

How AI Assistants are Moving the Security Goalposts

AI-based assistants or "agents" -- autonomous programs that have access to the user's computer, files, online services and can automate virtually any task -- are growing in popularity with developers and IT workers. But as so many eyebrow-raising headlines over the past few weeks have shown, these p...

via Krebs on Security