Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Compiler fails to catch implicit any #34

Closed
erkannt opened this issue Mar 15, 2022 · 3 comments
Closed

Compiler fails to catch implicit any #34

erkannt opened this issue Mar 15, 2022 · 3 comments

Comments

@erkannt
Copy link

erkannt commented Mar 15, 2022

Not sure if this is a bug or me doing something wrong.

Looking at the implementation of Task.map I would have thought the code below shouldn't pass the type checking.

Any ideas welcome 😇

import { serve } from "https://deno.land/std@0.120.0/http/server.ts";
import { pipe } from "https://deno.land/x/fun@v1.0.0/fns.ts";
import * as T from "https://deno.land/x/fun@v1.0.0/task.ts";

type User = {
  name: string;
  mail: string;
};

const render = (user: User) => `Name: ${user.name}, email: ${user.mail}`;

const getJson = (url: string) => async () => {
  const jsonResponse = await fetch(url);
  const jsonData = await jsonResponse.json();
  console.log(jsonData);
  return jsonData;
};

const handler = () =>
  pipe(
    "https://api.github.com/users/denoland",
    getJson, // This returns a Task<any>
    T.map(render), // Why isn't the compiler noticing that `render` needs to be passed a `User`?
    T.map((s) => new Response(s))
  );

await serve(handler());
@pixeleet
Copy link
Collaborator

    getJson, // This returns a Task<any>
    T.map(render), // Why isn't the compiler noticing that `render` needs to be passed a `User`?

getJson here indeed returns a Task<any> and from the perspective of render that's fine, because any is literally assignable to anything.

This is expected behavior and not related to this library at all, but how the TS type system works.

@baetheus
Copy link
Owner

@pixeleet Is correct. Let's walk through the types to confirm, though.

const handler = () =>
  pipe(
    // This is just a string
    "https://api.github.com/users/denoland", 
    // pipe takes getJson `string => Task<any>` and returns `Task<any>`
    getJson,
    // Let's build out T.map first. The type for T.map is
    // `<A, I>(fai: (a: A) => I) => (ta: Task<A>) => Task<I>`
    // The render function (which we plug in for fai) has type `(user: User) => string`
    // That means T.map(render) has type:
    // `(ta: Task<User>) => Task<string>`
    // But we pass it a `Task<any>`.
    
    // We know that we can use an object of type `any` wherever we can use a `User` but
    // we don't know if we can use `Task<any>` wherever we can use `Task<User>`
    // If we can then that means that the `Task` type is *covariant*. Here, we say,
    // a type *T* is covariant if for types *A* and *B*, with *A* being a subset of *B*, *T<A>* is a subset of *T<B>*.

    // It turns out that functions are a little weird when it comes to covariance and contravariance.
    // For function A to be a subtype of function B the arguments of function A must be contravariant
    // to the arguments of function B *and* the return type of function A must be covariant to the
    // return type of function B.

    // Since we don't have any arguments for the `Task` type we only need the return type
    // of `Task<any>` to be covariant to the return type of `Task<User>`. This means we
    // only need to prove that `Promise<any>` can be used anywhere `Promise<User>` can.
    // This should be trivial to see.
    
    // Ultimately, from a type theory perspective, it is 100% ok to pass a `Task<any>` to the function
    // `(ta: Task<User>) => Task<string>`.
    T.map(render), // T.map has type 
    T.map((s) => new Response(s))
  );

Now, we know that the compiler isn't messing up, but the question of how to make this safe is still hanging around. I can think of a few options.

  1. You can set the noImplicitAny or noImplicitReturns compiler flags in your typescript configuration json.
  2. You could simply add the correct type to your getJson function const getJson = (url: string): Task<unknown> => ....

Hope this helps!

@erkannt
Copy link
Author

erkannt commented Mar 15, 2022

Thank you for the elaborate answers. Sorry about the newbie question.

With your answers I was able to find the source of my confusion. I'm used to working in a codebase that uses no-unsafe-return in it's eslint rules.

Appears that deno lint can't support that rule as it requires the types to be available to it. deno lint issue.

Not sure how I'll end up resolving this, but big thank you for your help!

@erkannt erkannt closed this as completed Mar 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants