9

If I have an array a where each element is an object consisting of two properties first and second, how should I declare `a's type such that the following is always satisfied?

 ForAll(x in a)(type(x.first) == T iff type(x.second) == (T => string))

I.e., I want to make sure that, for example, a[3].second(a[3].first) is typesafe.

Barr J
  • 10,636
  • 1
  • 28
  • 46
Hossam El-Deen
  • 972
  • 1
  • 13
  • 27

2 Answers2

10

The element type of the array would need to be an "existential type", which we could write in pseudocode as exists T. { first: T, second: (arg: T) => string }. TypeScript currently does not support existential types natively.

One potential workaround is to encode existential types using closures as explained in this answer. If you don't want to use real closures at runtime, you can use a utility library that provides a type definition for existential types (based on an encoding of type functions using indexed access types) and functions to produce and consume existential types that perform type casts but are just the identity at runtime:

// Library
// (Based in part on https://bitbucket.org/espalier-spreadsheet/espalier/src/b9fef3fd739d42cacd479e50f20cb4ab7078d534/src/lib/type-funcs.ts?at=master&fileviewer=file-view-default#type-funcs.ts-23
// with inspiration from https://github.com/gcanti/fp-ts/blob/master/HKT.md)

const INVARIANT_MARKER = Symbol();
type Invariant<T> = {
    [INVARIANT_MARKER](t: T): T
};

interface TypeFuncs<C, X> {}

const FUN_MARKER = Symbol();
type Fun<K extends keyof TypeFuncs<{}, {}>, C> = Invariant<[typeof FUN_MARKER, K, C]>;

const BAD_APP_MARKER = Symbol();
type BadApp<F, X> = Invariant<[typeof BAD_APP_MARKER, F, X]>;
type App<F, X> = [F] extends [Fun<infer K, infer C>] ? TypeFuncs<C, X>[K] : BadApp<F, X>;

const EX_MARKER = Symbol();
type Ex<F> = Invariant<[typeof EX_MARKER, F]>;
function makeEx<F, X>(val: App<F, X>): Ex<F> { 
    return <any>val;
}
function enterEx<F, R>(exVal: Ex<F>, cb: <X>(val: App<F, X>) => R): R { 
    return cb(<any>exVal);
}

// Use case

const F_FirstAndSecond = Symbol();
type F_FirstAndSecond = Fun<typeof F_FirstAndSecond, never>;
interface TypeFuncs<C, X> { 
    [F_FirstAndSecond]: { first: X, second: (arg: X) => string };
}

let myArray: Ex<F_FirstAndSecond>[];
myArray.push(makeEx<F_FirstAndSecond, number>({ first: 42, second: (x) => x.toString(10) }));
myArray.push(makeEx<F_FirstAndSecond, {x: string}>({ first: {x: "hi"}, second: (x) => x.x }));
for (let el of myArray) { 
    enterEx(el, (val) => console.log(val.second(val.first)));
}

(If there's enough interest, I may properly publish this library...)

Matt McCutchen
  • 28,856
  • 2
  • 68
  • 75
1

If you want an array where first will always have the same type, this will do

interface IArrayGeneric<T> {
    first: T;
    second: (arg: T) => string;
}

const a: Array<IArrayGeneric<   type   >>;

This will ensure that you can't put any object into a that doesn't satisfy the above requirements, but will also constrain T to one specific type you choose.

EmandM
  • 922
  • 6
  • 12
  • 5
    Nope, `IArrayGeneric` will accept objects with mismatching `first` and `second`. E.g., `a.push({ first: 42, second: (x: {y: string}) => x.y})` compiles. – Matt McCutchen Aug 13 '18 at 06:20
  • 4
    You are completely correct. This looks like a fairly common use case, I'm surprised there's not a fairly simple solution. – EmandM Aug 13 '18 at 06:40