0

I'm creating a connection to an AWS mysql database like so:

const config = {
  host: process.env.RDS_HOSTNAME,
  user: process.env.RDS_USERNAME,
  password: process.env.RDS_PASSWORD,
  port: 3306,
  database: process.env.RDS_DB_NAME,
}

const db = mysql.createConnection(config) // config gets highlighted

But I get the following error:

Argument of type '{ host: string | undefined; user: string | undefined; password: string | undefined; port: number; database: string | undefined; }' is not assignable to parameter of type 'string | ConnectionConfig'.

Type '{ host: string | undefined; user: string | undefined; password: string | undefined; port: number; database: string | undefined; }' is not assignable to type 'ConnectionConfig'.

    Types of property 'host' are incompatible.
      Type 'string | undefined' is not assignable to type 'string'.
        Type 'undefined' is not assignable to type 'string'.ts(2345)

Earlier on, I had a problem with the port coming from .env. When I switched to hardcoding the port, I get this.

I don't understand what the problem is nor how to solve it.

1 Answer 1

2

The problem is process.env is declared in @types/node as:

// process.d.ts
   ...
   interface ProcessEnv extends Dict<string> {}
   ...
   env: ProcessEnv

// global.d.ts
    interface Dict<T> {
        [key: string]: T | undefined;
    }
    ...  

As you may notice the result of any lookup in env is string | undefined. While createConnection expects string at least for host property.

You have several options to to ensure the compiler that passed config is ok:

  • if you're absolutely sure all env variables are correctly set just typecast your config:
type NoUndefinedField<T> = {
  [P in keyof T]: Exclude<T[P], undefined>;
};

createConnection(config as NoUndefinedField<typeof config>)

Update

Quick explanation:

Here we're using <T> generics to abstract over concrete type, mapped type [P in keyof T] to iterate through all properties ([P in keyof T]) of type T and built-in utility type Exclude<> to remove undesired types from each property of type T[K]. And after we removed all undefined types from the properties of typeof config we typecast config value to this type.

Step by step explanation:

So, we have an object (value) config with type:

type ConfigUndefined = {
  host: string | undefined,
  username: string | undefined,
  port: number,
}

declare const config: ConfigUndefined

but we need an object with another type that mysql create method can digest:

type Config = { // defined somewhere inside `mysql` library
  host: string,
  username: string,
  port: number,
}

the fastest way to ensure the compiler that we know better what is the type of our value in runtime is type assertion. We import Config type from the mysql library and assert the compiler that our config object has absolutely the same type as it expects:

mysql.createConnection(config as Config)

playground link

So far so good. createConnection is satisfied and we can stop here. Though this approach is quite brittle. What if mysql's library maintainer adds later another required property to the Config type? Our code still asserts the compiler that our config: ConfigUndefined value has absolutely the same type as Config but in fact it doesn't anymore.

declare function createConnection(config: Config): void
declare const config: ConfigUndefined

type ConfigUndefined = {
    host: string | undefined,
    username: string | undefined,
    port: number
}

type Config = {
    host: string,
    username: string,
    port: number,
    smthing: string
}

createConnection(config as Config) // ok. compiler is stil happy

playground link

Well, after the runtime error we can investigate the problem, add smthing: process.env.SMTHING into our config object and we're good again. Till the next time.

Wouldn't it be better if we got that error in compile time. Without even running our code? So we cannot rely directly on the mysql's Config type. We should make the compiler check whether our real type is assignable (compatible) with Config instead. To make it work we should make the compiler to calculate the exact shape of your config object type but without those pesky undefined types in each property.

We should somehow iterate over each property of the type ConfigUndefined and remove undefined type if there is one.

Let's start with a simple type. string | undefined is a union type. To remove undefined from string | undefined type we can use conditional types and namely their distribution property for that:

type StringOrUndefined = string | undefined
type RemoveUndefined<T> = T extends undefined ? never : T

type StringWoUndefined = RemoveUndefined<StringOrUndefined>
// type StringWoUndefined ~ string

luckily we have Exclude utility type in standard library that does absolutely the same for arbitrary types we want to exlude. And we can rewrite our StringWoUndefined type as:

type StringOrUndefined = string | undefined
type StringWoUndefined = Exclude<StringOrUndefined, undefined>

Now we have to iterate over ConfigUndefined properties and remove undefined from each one. Thats where mapped types and keyof operator come in handy:

type ConfigWoUndefined = {
  // keyof ConfigUndefined = "host" | "username" | "port"
  [K in keyof ConfigUndefined]: Exclude<ConfigUndefined[K], undefined>
}
// type ConfigWoUndefined = {
//   host: string,
//   username: string,
//   port: number,
// }

Here for each K in "host" | "username" | "port" typescript calculates ConfigUndefined[K] indexed access type and with the help of Exclude utility type removes undefined type from it.

Though the type looks pretty useful and if we ever want to reuse the same functionality we'll have to retype it again changing only ConfigUndefined to some other type we want to remove undefineds from. Looks kind of a good job for generic types. That's kind of a function on type-level that takes type arguments and return another modified type:

// our `old` ConfigWoUndefined
// type ConfigWoUndefined = {
//   [K in keyof ConfigUndefined]: Exclude<ConfigUndefined[K], undefined>
// }

type NoUndefinedField<T> = {
  [K in keyof T]: Exclude<T[K], undefined>
}

type ConfigWoUndefined = NoUndefinedField<ConfigUndefined>

So far so good. But we still have a distinct object (value) config and type ConfigUndefined. And we have to keep them in sync. That's what typeof operator is for:

declare const process: { env: { [k: string]: string | undefined } }

const config = {
  host: process.env.HOST,
  username: process.env.USER,
  port: 3000,
}

type A = typeof config
// type A = {
//   host: string | undefined,
//   username: string | undefined,
//   port: number,
// }

And we finally can get rid of ConfigUndefined type.

So to sum everything up:

createConnection(config as NoUndefinedField<typeof config>)

Here we're getting the exact type of value config with typeof config, remove from each of its properties undefined type with NonUndefinedField and typecast our config value to this modified type. That makes typescript to check whether NoUndefinedField<typeof config> is assignable to mysql's Config expected type.

And though that may make sense if we're absolutely sure that all required envs are defined as they should. The better practice is still to use runtime checks to narrow the types. Just as it is done below in assertion function.


function assertConfig<T>(config: T): asserts config is NoUndefinedField<T> {
  for (const key in config) {
    if (typeof config[key] === 'undefined') 
      throw new Error(`Config check failed. Key "${key}" is undefined.`)
  }
}

playground link

Though, I believe for production code assertion function is the proper way to exit early on database misconfiguration.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.