Note: This is a draft and not well proofread. Likely contains errors.
class Book { id: string }
class Film { imdbId: string }
async function fetchMetadata(item: Book | Film) {
if (item instanceof Book)
return fetch(`https://blah.org/${item.id}`).then(r => r.json());
else if (item instanceof Film)
return fetch(`https://blah.org/${item.imdbId}`).then(r => r.json());
}
Yaaay! It can deduce the type after an instanceof
!
type Suites = 'Hearts' | 'Clubs' | 'Diamonds' | 'Spades';
function Card(suite: Suites) {
this.suite = suite;
}
const card = new Card('Hearts');
type GenericMetadataRecord = {
identifier: string
mediatype: string
}
type BookMetadataRecord = GenericMetadataRecord & {
mediatype: 'texts'
title: string
/** @deprecated Use openlibrary_edition */
openlibrary?: string
openlibrary_edition?: string
openlibrary_work?: string
}
function getOLID(book: BookMetadataRecord) {
return book.openlibrary_edition || book.openlibrary_work;
}
getOLID({ identifier: 'foo', mediatype: 'texts', title: 'Hello' });
type TreeNode<T> = T & { children?: TreeNode<T>[] };
function recurForEach<T>(node: TreeNode<T>, fn: (node: TreeNode<T>) => void) {
fn(node);
if (node.children)
for (const child of node.children) recurForEach(child, fn);
}
const tree = {
short: 'J',
label: 'Political Science',
children: [
{
short: 'J--',
label: ' General legislative and executive papers'
}
]
};
recurForEach(tree, x => console.log(x.label));
Ah, won't aut-detect deeper; need to pre-define ShelfNode:
type TreeNode<T> = T & { children?: TreeNode<T>[] };
function recurForEach<T>(node: TreeNode<T>, fn: (node: TreeNode<T>) => void) {
fn(node);
if (node.children)
for (const child of node.children) recurForEach(child, fn);
}
type ShelfNode = TreeNode<{short: string, label: string}>;
const tree = {
short: 'J',
label: 'Political Science',
children: [
{
short: 'J--',
label: ' General legislative and executive papers',
children: [
{
short: 'J--',
label: ' General legislative and executive papers'
}
]
}
]
};
recurForEach<ShelfNode>(tree, x => console.log(x.label));
Introduced in (I think) TS 4.1 ; not working in VS Code yet for me. But awesome!
const x: `${number}%` = '33%';
/** @type {`${number}%`} */
const x = '33%';
You can do any of this stuff with just jsdoc; makes setup a breeze! And keeps the compile-time-only type info separated from the code.
For the most part, just need to replace type Foo = ...
with /** @typedef {...} Foo */
, and add // @ts-check
to the top of your JS file.
See https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html .
Here are some basic examples:
// @ts-check
// ^ Don't forget the @ts-check! This is what makes VS Code check for type errors in .js files.
// You can put anything that would be in a TS `type` definition here
/** @typedef {'Hearts' | 'Clubs' | 'Diamonds' | 'Spades'} Suite */
// This syntax is useful for interface/pojos:
/**
* @typedef {object} Card
* @property {Suite} suite
* @property {'ace'|2|3|4|5|6|7|8|9|'jack'|'queen'|'king'} value
*/
// How to specify the type of a fixed value
/** @type {Card} */
const card = { suite: 'Hearts', value: 'ace' };
TODO
What I want to write:
type Suite = 'Hearts' | 'Clubs' | 'Diamonds' | 'Spades';
class Card {
constructor(suite: Suite) {
this.suite = suite;
}
}
What I have to write:
type Suite = 'Hearts' | 'Clubs' | 'Diamonds' | 'Spades';
class Card {
suite: Suite;
constructor(suite: Suite) {
this.suite = suite;
}
}
I get why, but this is a non-essential shift from normal JS syntax. It would be wonderful if it supported both.
Actually is possible with TS-JSDoc:
/** @typedef {'Hearts' | 'Clubs' | 'Diamonds' | 'Spades'} Suite */
class Card {
/**
* @param {Suite} suite
*/
constructor(suite) {
this.suite = suite;
}
}
What I want to write:
class Book { id: string }
class Film { imdbId: string }
async function fetchMetadata(book: Book) {
return fetch(`https://blah.org/${book.id}`).then(r => r.json());
}
async function fetchMetadata(film: Film) {
return fetch(`https://blah.org/${film.imdbId}`).then(r => r.json());
}
const items = [new Book(), new Film()];
Promise.all(items.map(fetchMetadata));
What I have to write:
class Book { id: string }
class Film { imdbId: string }
async function fetchMetadata(book: Book);
async function fetchMetadata(film: Film);
async function fetchMetadata(item: Book | Film) {
if (item instanceof Book)
return fetch(`https://blah.org/${item.id}`).then(r => r.json());
else if (item instanceof Film)
return fetch(`https://blah.org/${item.imdbId}`).then(r => r.json());
}
const items = [new Book(), new Film()];
Promise.all(items.map(fetchMetadata));
The syntax for this is just very odd. I would love to be able to just do multiple dispatch.
Also, what I would want to write:
class Book { id: string }
class Film { imdbId: string }
async function fetchMetadata(item: Book | Film) {
return fetch(`https://blah.org/${item.id ?? item.imdbId}`).then(r => r.json());
}
But TS' inference system isn't good enough.
What I want to do:
async function fetchSomething(url: string): Object {
return fetch('foo').then(r => r.json());
}
// The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<Object>'?ts(1064)
You know an async function always returns a promise, so why don't you just do that? What I have to do:
async function fetchSomething(url: string): Promise<Object> {
return fetch('foo').then(r => r.json());
}
const x = [[1,2,3]].flat();
const y = [[1,2,3]].flatMap(x => x**2);
What I want to write:
function forEach(fn: any => void) { ... }
// OR
function forEach(fn: (any) => void) { ... }
// OR
function forEach(fn: function(any): void) { ... }
What I have to write:
function forEach<T>(fn: (node: any) => void) { ... }
The only valid way of doing this is nice in that it's consistent, but the other 2 ways are also consistent with the ways functions are defined in js/ts. Limiting this to only one specific way feels inconsistent.
class Book { titles: string[] }
function foo(book?: Book) {
// Why doesn't this throw an error? I'm ignoring the fact that this can be
// null
return book.titles.join('; ');
}
I want to have to write:
class Book { titles: string[] }
function foo(book?: Book) {
return book?.titles.join('; ');
}
What I want to write:
type Suites = 'Hearts' | 'Clubs' | 'Diamonds' | 'Spades';
function getSuiteEmoji(suite: Suites) {
switch(suite) {
case 'Hearts': return '♥';
// Want a compile-time ERROR: Incomplete switch case
}
}
In order to get this to error at compile time, I either have to add a typescript eslint plugin @typescript-eslint/switch-exhaustiveness-check
, or write something like:
function find<T>(arr: [], predicate: (el: T) => boolean) { return false; }
function find<T>([head, ...rest]: T[], predicate: (el: T) => boolean) {
return predicate(head) || find(rest, predicate);
}
With Literal types and destructuring, this feels like such a natural way to write code :(
What I want to write:
type Person = { firstName: String, lastName: String }
const x = { firstName: 'Jimmy', lastName: 'Carr' }
if (x instanceof Person) {
// blah
}
There's no real type information available at runtime :( It all goes away. I could do something like this:
type Person = { firstName: String, lastName: String };
function matchesType<T>(x: T) {
return true;
}
const x = { firstName: 'Jimmy', lastName: 'Carr' };
const y = { x: 3, y: 3 };
if (matchesType<Person>(x)) {
// blah
}
if (matchesType<Person>(y)) {
// blah
}
But this is a compile time check that returns an error; if I was writing a library to deal with dynamic content (e.g. if x
was fetched from a network), this function would always return true after this function was compiled. In fact, it's equivalent to:
type Person = { firstName: String, lastName: String };
const x = { firstName: 'Jimmy', lastName: 'Carr' };
const y = { x: 3, y: 3 };
if (x as Person) {
// blah
}
if (y as Person) { // <- Compile-time error
// blah
}
The only recommended way I can find for this on the official docs, is:
type Person = { firstName: String, lastName: String };
function isPerson(x: any): x is Person {
return 'firstName' in (x as Person) && 'lastName' in (x as Person);
}
const x = { firstName: 'Jimmy', lastName: 'Carr' };
const y = { x: 3, y: 3 };
if (isPerson(x)) {
// blah
}
if (isPerson(y)) {
// blah
}
What I want to write:
const lendingInfo = {
status: "OK",
lending_status: {
is_lendable: true,
is_printdisabled: true,
is_readable: false,
is_login_required: false,
max_lendable_copies: 7,
available_lendable_copies: 0,
max_browsable_copies: 7,
available_browsable_copies: 0,
max_borrowable_copies: 6,
available_borrowable_copies: 0,
users_on_waitlist: 0,
last_waitlist: null,
copies_reserved_for_waitlist: 0,
upgradable_browses: 0,
active_browses: 0,
last_browse: null,
next_browse_expiration: null,
active_borrows: 6,
last_borrow: "2020-10-16 17:08:34",
next_borrow_expiration: "2020-10-20 16:01:14",
orphaned_acs_loans: 1,
available_to_browse: false,
available_to_borrow: false,
available_to_waitlist: true,
user_is_printdisabled: false,
user_loan_count: 0,
user_at_max_loans: false,
user_has_browsed: false,
user_has_borrowed: false,
user_loan_record: [],
user_on_waitlist: false,
user_can_claim_waitlist: false,
user_has_acs_borrowed: false
}
};
switch(lendingInfo.lendingStatus) {
case { is_readable: true, available_to_browse: false }: 'publicly available';
case { is_lendable: true, available_to_browse: true, }: 'available to browse'
default: 'not available';
}
Can't recall if I got this working or not; but there are better attempts at this by other online if you Google it! TypeScript's type system is, I believe, Turing complete :)
type Zero = { prev: 'nil' }
type PNum = { prev: PNum } | Zero
type Incr<T extends PNum> = { prev: T }
type Decr<T extends PNum> = T extends Zero ? Zero : T['prev']
type One = Incr<Zero>
type Two = Incr<One>
type Three = Incr<Two>
type Four = Incr<Three>
type Five = Incr<Four>
type Six = Incr<Five>
type Seven = Incr<Six>
type Eight = Incr<Seven>
type Nine = Incr<Eight>
type Ten = Incr<Nine>
type Sum<T1 extends PNum, T2 extends PNum> =
T1 extends Zero ? T2 :
T2 extends Zero ? T1 :
{ prev: Sum<Decr<T1>, T2> }
// This seems to have an error, but is still working 🤷♂️
type Product<T1 extends PNum, T2 extends PNum> =
T1 extends Zero ? Zero :
T2 extends Zero ? Zero :
T1 extends One ? T2 :
T2 extends One ? T1 :
{ prev: Decr<Sum<Sum<T1, T1>, Product<T1, Decr<Decr<T2>>>>> }
const one: One = { prev: { prev: 'nil' } };
const two: Two = { prev: { prev: { prev: 'nil' } } }
const three: Three = { prev: { prev: { prev: { prev: 'nil' } } } }
// hover over the left side to get the answer in the type!
type X = Sum<Two, One>
type Y = Product<Three, Three>
type Z = Sum<Five, Five>