2

I have a type MyType

type MyType = {
  a?: {c: string},
  b?: {e: string},
}

that contains optional nested objects. I want to create a function getDefaultValue that accepts a key K of MyType and returns a default representation of the nested object that is specified by key K.

function getDefaultValue<K extends keyof MyType>(key: K):Required<MyType>[K] {
  if (key === 'a') return {c: 'hello'}
  return {e: 'hello'}
}

const variable: Required<MyType>['a'] = getDefaultValue('a')

I get the following typescript errors:

TS2322: Type '{ c: string; }' is not assignable to type 'Required<MyType>[K]'.
  Type '{ c: string; }' is not assignable to type '{ c: string; } & { e: string; }'.
    Property 'e' is missing in type '{ c: string; }' but required in type '{ e: string; }'.
TS2322: Type '{ e: string; }' is not assignable to type 'Required<MyType>[K]'.
  Type '{ e: string; }' is not assignable to type '{ c: string; } & { e: string; }'.
    Property 'c' is missing in type '{ e: string; }' but required in type '{ c: string; }'.

2 Answers 2

2

Matching the types of returned values with generic parameters can be a bit tricky, but in this case you can avoid the problem altogether by putting the defaults in an object

const defaults = {
  a: { c: 'hello' },
  b: { e: 'hello' },
}

and declaring getDefaultValue like this:

function getDefaultValue<K extends keyof MyType>(key: K) {
  return defaults[key]
}

const test1 = getDefaultValue('a') // inferred type: {c: string}
const test2 = getDefaultValue('b') // inferred type: {e: string}

Missing default values will be signalled by a type error at defaults[key].

TypeScript playground

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

4 Comments

We are using strict linter rules which oblige to set the return type of the function. Is there any type (except any =)) which describes it or is it just working because of inferred types?
@AndriiDobrianskiy You can use the Required<MyType>[K] return-type signature from the question and it will still work.
Thanks, still, things like const test1 = getDefaultValue('a' as "a" | "b") does not work . But it is for sure reasonable.
@AndriiDobrianskiy Typically in functions like this the key will have a literal type because getDefaultValue will get called with a constant string argument, but if even it's a union, you'll get a union of possible result types, which seems the desired type to me. You indeed won't be able to statically infer the actual type from the value at run-time in case the type is too wide, but you don't really need to for this function to be useful.
0

I think it is impossible to do it with TypeScript as you are trying to use runtime values instead of types to create a generic.

Please consider the following example and try to answer the questions:

const key: 'a' | 'b' = 'a';
const variable: Required<MyType>['a'] = getDefaultValue(key)
  1. How to guarantee that the result type is basically Required<MyType>['a'] not Required<MyType>['b']
  2. How typescript should understand that K extends keyof MyType is a type of one value?

P.S. As a hack you can specify any as a return type, but it is not something that you need to do with TypeScript

type MyType = {
  a?: {c: string},
  b?: {e: string},
}

type MyKeys = keyof MyType;

function getDefaultValue(key: MyKeys): any {
  if (key === 'a') {
    return {c: 'hello'};
  }
  return {e: 'hello'}
}

const variable: Required<MyType>['a'] = getDefaultValue('a')

P.P.S. I would recommend creating a function that will return a default value for the whole object and simply grab what you need

function getDefaultValue(): Required<MyType> {
  return {
    a: {c: 'hello'},
    b: {e: 'hello'},
  };
}
const variable = getDefaultValue().a;

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.