0

In the following code (Playground) I try to make autocompletion and function signature inference work within personTypeX({...}) at the same time but I can only achieve either of these but not both.

Is it even possible?

Here are the requirements:

  • If a person (= the input) has a firstName, then it must be a string.
  • If a person has an age, then it must be a number.
  • ...
  • If a person has a property which is neither firstName, lastName, age, favouriteFood nor getLegCount then the value at that property must itself be a Person.

There are 3 overall additional requirements

  • Invalid input must be detected (at any level in the input) and marked in the correct location. see below: works with personType1() and personType2()
  • Autocompletion for all keys in firstName, ..., getLegCount must be provided within personTypeX({}) see below: currently only works within personType2({})
  • If the key (here only getLegCount) enforces its value to be a function, then the function signature must be correctly inferred see below: currently only works within personType1({})
type FixedType = {
  firstName: string,
  lastName: string,
  age: number,
  favouriteFood: string[],
  getLegCount: (numHeads: number) => number,
};

type PersonType<T> = {
  [K in (keyof T | keyof FixedType)]?:
    K extends keyof FixedType
      ? FixedType[K]
      : K extends keyof T
        ? T[K] extends object
          ? PersonType<T[K]>
          : PersonType<{}>
        : never
}

// ---- ---- ---- ---- ----

function personType1<T extends PersonType<T>>(t: T) { // signature version 1
  return t;
}

const person11 = personType1({
  firstName: 'Mark',
  lastName: 'Antony',
  holger: {
    age: 12,
    paul: {
      // <-- type 'a' for autocompletion of 'age' => NOT WORKING
      favouriteFood: ['cheese'],
      firstName: 'Paul',
      getLegCount: (numHeads) => 2, // <-- hover over 'numHeads' => numHeads: number correctly inferred => WORKING
    }
  },
});

// ----

function personType2<T extends PersonType<T>>(t: T & PersonType<T>) { // signature version 2
  return t;
}

const person1 = personType2({
  firstName: 'Mark',
  lastName: 'Antony',
  holger: {
    age: 12,
    paul: {
      // <-- type 'a' for autocompletion of 'age' => WORKING
      favouriteFood: ['cheese'],
      firstName: 'Paul',
      getLegCount: (numHeads) => 2, // <-- hover over 'numHeads' => numHeads: any inferred => NOT WORKING
                                    // that's probably bc it's the most common signature for
                                    //   (property) getLegCount: ((numHeads: any) => number) & ((numHeads: number) => number)
                                    // but I have no clue how to fix this
    }
  },
});

3
  • 1
    I have noticed that you have several similar questions. Could you please provide more information what are you expect from function argument? What type restrictions should be applied to it ? Commented Jun 13, 2022 at 7:22
  • Hello, Thank you for answering. I created an improved Playground here. This playground demonstrates my problem, that I can either make autocompletion or function argument inference work, but not both at the same time. :( Commented Jun 13, 2022 at 15:14
  • I came to the conclusion that this may be a bug. Commented Jun 14, 2022 at 22:47

2 Answers 2

2

In short what you are asking for is impossible at the present moment (as far as my understanding goes).

There is a lot of circular logic here, which it a limitation of TS (and just an affront to humanity in terms of managing/understanding this type of code). We effectively circumvent it using a generic inside a generic, but we are still limited by it. Namely, from my understanding, we can only choose one:

  • Retrieve the data type of the parameter, validate and make that the data type of the parameter (IE. autocompletion)
  • Validate the parameter is suitable, cast upon itself inferenced types (getLegCount: (numHeads) => 2), make that the data type of the parameter (IE. argument inferencing on non-explicit types)

An example of this is when we go back to how generics are made

function example<T>(t: T)

Here we can either explicitly define T to prescribe t to follow, or we can infer T from t

When we do something like this:

function personType<T extends PersonTypeNew<T>>(t: T){...}

We are making circular logic, and at some point TS has to choose T or t. (even though they are dependent on each other, they create different outcomes).

  • If we infer the arguments (and thereby typing getLegCount: (numHeads), we have inferred age to be nothing (because it's not in the arguments), and there is no need to provide autocomplete to age (or any other keys).
  • Otherwise, the default behavior provides autocomplete

^ This is technically a super super simplification. This is actually supported in simple type objects (ie. non-circular) with something called partial inferencing, which will allow autocomplete and argument inferencing.

If this is confusing, there are some examples in the playground.



Anyways, I rewrote it from the ground up. Supports both autocompletion and argument inferencing, but only the former if the latter isn't present.

type PersonTypeNew<T> = {
  // We have to overwrite any keys, otherwise we could incorrectly infer.
  // Hence Omit<T, keyof FixedType>, we use & Partial<FixedType> for correct autocompletion
  [K in keyof (Omit<T, keyof FixedType> & Partial<FixedType>)]: 
    K extends keyof FixedType
      ? FixedType[K]
      : (Omit<T, keyof FixedType> & Partial<FixedType>)[K] extends Record<string, any>
        ? PersonTypeNew<(Omit<T, keyof FixedType> & Partial<FixedType>)[K]>
        : "Invalid Value!"
}

This also adds autocompletion (when you aren't inferencing) on the first layer.

It's a lot more terse without autocompletion support (since we just validate without intersecting two object types)

type PersonTypeNewNoAutocomplete<T> = {
  [K in keyof T]?: 
    K extends keyof FixedType
      ? T[K] extends FixedType[K]
        ? T[K]
        : FixedType[K]
      : PersonTypeNewNoAutocomplete<T[K]>
}

Check out Playground for Examples

There are open issues discussing this, this goes into further detail on the context engine, priority, other relevant topics to circular types:

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

1 Comment

Nice answer @Cody Duong
2
+300

Check this out, it works by using Partial<FixedType> for autocompletion, K extends keyof FixedType ? FixedType[K] for error checking and a recursive generic type for nested objects.

Playground

code:

type TypeCheckedInferedPerson<T> = Partial<FixedType> & PersonType<T>

type PersonType<T extends Partial<FixedType>> = {
  [K in keyof T]: K extends keyof FixedType ? FixedType[K] : TypeCheckedInferedPerson<T[K]> 
}

function personType2<T extends PersonType<T>>(t: TypeCheckedInferedPerson<T>) { 
  return t;
}

Result: result

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.