Skip to content

Instantly share code, notes, and snippets.

@masaeedu
Last active January 17, 2024 23:11
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save masaeedu/becdacf5fdebadf33f7e9b8f2af1e9d8 to your computer and use it in GitHub Desktop.
Save masaeedu/becdacf5fdebadf33f7e9b8f2af1e9d8 to your computer and use it in GitHub Desktop.

Thank you for the helpful response and pointers. I think I need to take my time to sort this out, thanks a lot and I'll come back with a better understanding, hopefully!

I did my homework. Now I'm not so certain about everything, so please take this with a grain of salt.

Typescript's generic functions seem to act like they're contravariant over the type parameter's type bound. I'm not entirely sure though.

Playground Link

// Assignment succeeds with F<unknown> -> F<string>
// Assignment fails with F<string> -> F<unknown>

const a1: <T extends unknown>(x: T) => T = (x) => x;
const b1: <U extends string>(y: U) => U = a1;
const c1: typeof a1 = b1; // error

const a2: <T extends unknown>(x: T) => string = String;
const b2: <U extends string>(x: U) => string = a2;
const c2: typeof a2 = b2; // error

declare const a3: <T extends unknown>() => T;
const b3: <U extends string>() => U = a3;
const c3: typeof a3 = b3; // error

@masaeedu This seems like the {co,contra}variance of the types is independent to whether you can do a {co,contra}map on it. May I ask, did I mess up again, or is this another unsoundness, or is this the way it is? Anyhow thank you so much for the helpful discussion! heart

@masaeedu
Copy link
Author

So "variance" is a property that type constructors have. Before we ask "is it co/contra variant", we should consider what type constructor we're asking the question about. A type constructor looks like this:

type F<X> = ... some type expression ...

There's various ways one could extract type constructors from the examples given (i.e. of which the ascribed types are instantiations), but I think it would involve less guesswork if you gave it some thought and formulated it explicitly.

@scorbiclife
Copy link

scorbiclife commented Mar 13, 2023

Okay, I should have been more explicit.

// If I make type constructors and instantiate them,
// I can't assert anything about the variance
// because the variance inference is buggy due to this issue:
// https://github.com/microsoft/TypeScript/issues/53210
type F1<T> = <T_ extends T>(x: T_) => T_;
const a1_: F1<unknown> = (x) => x;
const a2_: F1<string> = a1_;
const a3_: F1<unknown> = a2_;

// So let's instantiate the generic types manually.

// type F1<T> = <T_ extends T>(x: T_) => T_;
const a1: <T extends unknown>(x: T) => T = (x) => x;
const b1: <U extends string>(y: U) => U = a1;
const c1: typeof a1 = b1; // error
// F1<unknown> is a subtype of F1<string>

// type F2<T> = T<T_ extends T>(x: T_) => string
const a2: <T extends unknown>(x: T) => string = String;
const b2: <U extends string>(x: U) => string = a2;
const c2: typeof a2 = b2; // error
// F2<unknown> is a subtype of F2<string>

// type F3<T> = T<T_ extends T>() => T_
declare const a3: <T extends unknown>() => T;
const b3: <U extends string>() => U = a3;
const c3: typeof a3 = b3; // error
// F3<unknown> is a subtype of F3<string>

// In all cases of F1, F2, F3,
// string is a subtype of unknown but F<unknown> is a subtype of F<string>.
// this seems natural because
// `<U extends unknown>(type expression using U)` is assignable to a superset of
// what's assignable from `<U extends string>(type expression using U)`
// but I'm not entirely sure.

Link

@scorbiclife
Copy link

scorbiclife commented Mar 13, 2023

I brought this up because I cannot find a contramap for F1 and F3 in the above post,
so I was not sure how the {co,contra}variance of type constructors are related to {co,contra}variant functors, respectively.

Another (probably more persuasive) way to figure out whether something is covariant or contravariant is to try inhabiting the following two types:

type F<A> = <X extends A = A>(v: X) => never

const map
: <A, B>(ab: (a: A) => B, fa: F<A>) => F<B>
= (ab, fa) => b => undefined

const contramap
: <A, B>(ba: (b: B) => A, fa: F<A>) => F<B>
= (ba, fa) => b => undefined

And see which one you have more success with.

Edit: Here's my attempt at implementing contramap of these forms:

// Type inference on the genric form ContramapA1 is buggy,
// so I'll try to complete specific instances of A1.
type ContramapA1<A, B> =
    (ba: (b: B) => A, fa: <X extends A>(x: X) => X) =>
        <X extends B>(x: X) => X;

const contramapA1Ex1: ContramapA1<string, number> = (ba, fa) => 
    <X extends number>(x: X) => {
        throw new Error("I can't seem to implement this");
    }

type ContramapA3<A, B> =
    (ba: (b: B) => A, fa: <X extends A>() => X) =>
        <X extends B>() => X;

const contramapA3Ex1: ContramapA3<string, number> = (ba, fa) =>
    <X extends number>() => {
        throw new Error("I can't seem to implement this");
    }

Playground Link

@scorbiclife
Copy link

scorbiclife commented Mar 13, 2023

Assuming I didn't make a mistake in the previous posts here's an implementation of Lens that seems to work.

type Lens<in Ti extends { obj: unknown; val: unknown } = { obj: unknown; val: unknown }> = {
  get: <T extends Ti>(obj: T["obj"]) => T["val"];
  set: <T extends Ti>(val: T["val"], obj: T["obj"]) => T["obj"];
};

type ArrayLens<in Ui = unknown> = Lens<{ obj: Ui[]; val: Ui }>;

const arrayLensExample: ArrayLens = {
  get: (obj) => obj[0],
  set: (val, obj) => [val, ...obj.slice(1)],
};

const numberArrayLensExample: ArrayLens<number> = arrayLensExample;
const typeIsNumber = numberArrayLensExample.get([1, 2, 3]);

const stringArrayLensExample: ArrayLens<string> = arrayLensExample;
const typeIsStringArray = stringArrayLensExample.set("a", ["b", "c", "d"]);

Playground Link

@scorbiclife
Copy link

scorbiclife commented Mar 18, 2023

@masaeedu When you have time, could you take a look at my last post?

Edit: this one

Edit: I tried to wait the variance checking issue to resolve and wait for your feedback before posting to the generic values issue, but this snippet keeps popping up in my head, several times a day. I needed to dump this snippet in the generic values issue and move on with my life. I hope you understand...

@masaeedu
Copy link
Author

@nightlyherb No worries at all. Sorry I haven't responded yet, life keeps getting in the way. FWIW your new snippet with F1, F2, and F3 also raises a bunch of interesting (read thorny) questions, you seem to have quite a knack for doing that :). I made several attempts at writing up a response but kept running out of time.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment