Skip to content

Function definition parameter type inferenceΒ #61715

@ccorcos

Description

@ccorcos

πŸ” Search Terms

Function definition parameter type inference implicit any noImplicitAny

βœ… Viability Checklist

⭐ Suggestion

function a(x: number) {}
function b(x: string) {}

function f(x) {
  if (...) a(x)
  else b(x)
}

Ideally f would have an inferred type (x: number | string) => void as opposed to an implicit any, i.e. (x: any) => void

πŸ“ƒ Motivating Example

A common dependency injection pattern involves booting up (usually stateful) services, wrapping them up in an object, and passing it around as an argument.

// Instantiate stateful services
const db = new Database()
const auth = new Auth()
const analytics = new Analytics()

const env: Env = {db, auth, analytics}

function getUser(env: Env, args) { ... }
function createUser(env: Env, args) { ... }
function login(env: Env, args) { ... }
function logout(env: Env, args) { ... }

As your application grows, you might end up with dozens of different services and several contexts for running the application / calling these functions. You end up with a growing need to break apart this big Env type.

First, when writing tests, you might not want to boot up every service. So you'll start to define which services you need in the env argument.

function createUser(env: {db: Database}, args) { ... }

Now I can test this function without dealing with redis, cache, auth, etc.

Second, you might be running code from a few different contexts. Lets say that you want to run a few functions like createUser from the CLI without booting up a whole server. So now you might create a more than one different environment for dependency injection.

type ServerEnv = {db: Database, auth: Auth, analytics: Analytics}
type CliEnv = {db: Database, auth: Auth, aws: AWS}
type ServerOrCliEnv = { db: Database, auth: Auth }

When you have 20 different services and 5 different contexts (api server, background job server, cli, ci, etc.), these types become a mess.

And when you want to add a service dependency to a function, you need to find all of the callers of that function and plumb that type through everywhere -- it's a bit laborious. For example:

function a(env: {a}) {}
function b(env: {b}) {}

function x(env: {a, b}) {
  a(env)
  b(env)
}

function y(env: {a}) {
  a(env)
}

Now if I want need to update a with a new dependency, I need to propagate that change everywhere manually.

function a(env: {a, c}) {}

function x(env: {a, b, c}) {}
function y(env: {a, b}) {}

But in an ideal world, function parameters without type definitions aren't implicitly any but inferred.

function a(env: {a}) {}
function b(env: {b}) {}

function x(env) { // env is inferred to be {a,b}
  a(env)
  b(env)
}

function y(env) { // env is inferred to be {a}
  a(env)
}

This would allow type inference to handle all of the dependency injection for us and eliminate a lot of plumbing work of types.

πŸ’» Use Cases

  1. What do you want to use this for? See "Motivating Example"
  2. What shortcomings exist with current approaches? Probably some performance cost and some nontrivial backtracking.
  3. What workarounds are you using in the meantime? Manually plumb it yourself.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions