3

I am new to Typescript and I am running into an issue with Generic types. I am creating a library that reads our configuration. Due to our configuration being arbitrary JSON we are using the any type. However when a consumer uses this library they should know the shape of the data they expect and I want to make a function for retrieving information from the config that we can pass a type to remove the any type from the chain. Additionally I want this function that retrieves the value from the config to take an optional argument that will be the default value returned if the key is missing from the config. My problem is figuring out how to properly type this function so that the function's return type is either the type we pass in as a generic or the type of the default value while taking into account we may omit the default value argument entirely.

I have boiled down my use case to this really simple example.

/* Our config provider module */

//In our real use, config is loaded from a JSON file that can be an arbitrary shape so we must use any type here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const config: Record<string, any> = {
  'mode': 'alpha'
}

const getConfig = <Result, DefaultValue extends Result | undefined = undefined>(key: string, defaultValue?: DefaultValue): Result | DefaultValue => {
  const result = config[key] as Result
  return result ?? defaultValue
}

/* In another module */
// The consumer of the config knows what shape they are going to read so we want to break the `any` usage here:

// mode should be of type string | undefined since a default value was not provided
const mode = getConfig<string>('mode')

// mode2 should be of type string | string (or just string) since a default value was provided
const mode2 = getConfig<string>('mode', 'omega')

This example doesn't compile and I get the error:

Type 'Result | DefaultValue | undefined' is not assignable to type 'Result | DefaultValue'. Type 'undefined' is not assignable to type 'Result | DefaultValue'.ts(2322)

Any advice on how I can properly type my getConfig function so that if I pass in a type for the Result and the function will properly infer the type of the defaultValue argument (including undefined) and use that as part of it's return type, while also maintaining that if we do pass an actual value for the defaultValue then the type does not have undefined?

Note: I am aware that what I want will not actually validate the contents of the JSON data structure and will compile just fine but if when parsing the JSON the value doesn't match what we pass in as the generic type it could break at runtime. We will aim to validate the JSON later using a schema or something. For now I just want to figure out how to type this properly.

5
  • 1
    You might want to use ?? instead of || here because any falsey would trigger the short-circuiting and that might cause unexpected behavior. Commented May 5, 2022 at 16:17
  • I did not know about that operator! I definitely should use that instead! Thank you. I have updated my example to include it for future readers. Commented May 5, 2022 at 16:22
  • I like @DaveMeehan's answer but if you want a more fluent API, you could try this Commented May 5, 2022 at 16:38
  • Would undefined be a legal return value? In case config neither returns something nor was a defaultValue given? Commented May 5, 2022 at 16:42
  • Yes undefined is a legal return value if defaultValue is undefined. Basically I assumed I could infer the type of the defaultValue parameter and use it in the return type. Commented May 5, 2022 at 19:00

1 Answer 1

1

What you appear to be saying is that the return value is either the value from the config, or the defaultValue. Because defaultValue is optional, this means that return result ?? defaultValue also includes the possibility of undefined.

If you want undefined to be a possible (in the case that its missing from config AND defaultValue is not specified, then the return type must also include undefined.

If you want the return value to always be a value, and you aren't supplying one with defaultValue, then you need a type guard to prevent the possibility of undefined being returned, with a throw (which makes the return type effectively T | never.

It also appears that its not necessary to include the type assertion on config[key] to narrow from any to T, as that's implied by the return type.

const getConfig = <T>(key: string, defaultValue?: T): T => {
  const result = config[key] ?? defaultValue
  if (result === undefined) {
    throw "Can't be undefined without a default value"
  }
  return result
}

Playground Link

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

2 Comments

I should add that in the second case where defaultValue is supplied, you don't need to specify the generic parameter, as its inferred by the type of the defaultValue. const mode2 = getConfig('mode', 'omega')
Hmm, I guess what I want just ultimately is not possible. I am not sure why and I feel like it should be. I will take this and just ignore the 'undefined' part I guess.

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.