Search code examples
typescripttypescript-genericsconditional-types

How to enforce a property of an object, in an array of objects, to have a non empty string value?


Consider the following code

type EnforceNonEmptyValue<TValueArg extends string> = {
  value: TValueArg extends '' ? never : TValueArg
  type: 'substringMatch'
}

function identity<
  TValueArg extends string
>(
  object: EnforceNonEmptyValue<TValueArg>
) {
  return object
}

// rightfully complaints
identity({value: '', type: 'substringMatch'})
//---------^
//  `Type 'string' is not assignable to type 'never'.ts(2322)`

// works
identity({value: 'works', type: 'substringMatch'})

The function identity with the help of EnforceNonEmptyValue generic enforces that the object passed to the function has a non empty string value, i.e. the value of value property should not be an empty string ''

I want to extend this check for an array of objects

identityArray([
  {
    type: 'substringMatch',
    value: 'np-payment',
  },
  {
    type: 'substringMatch',
    value: '',
    //------^ should complain here
  },
])

i.e I want typescript to throw an error if any object in an array of objects has a value property's value as an empty string ''

But I've been having a hard time to make it work. How could we enforce it for an array of objects? ( with or without an identity function )


Solution

  • You can use a mapped array/tuple type as shown here:

    function identityArray<T extends string[]>(
      args: [...{ [I in keyof T]: EnforceNonEmptyValue<T[I]> }]
    ) { }
    

    The compiler can infer the type T from the homomorphic mapped type (see What does "homomorphic mapped type" mean?) {[I in keyof T]: EnforceNonEmptyValue<T[I]>}. I wrapped it with a variadic tuple type ([...+]) to give the compiler a hint that we'd like T to be inferred as a tuple type and not as an unordered array type, but you might not care either way.

    Anyway, let's test it:

    identityArray([
      {
        type: 'substringMatch',
        value: 'np-payment',
      },
      {
        type: 'substringMatch',
        value: '', // error!
      },
    ])
    // function identityArray<["np-payment", ""]>(
    //   args: [EnforceNonEmptyValue<"np-payment">, EnforceNonEmptyValue<"">]
    // ): void
    

    Looks good. The compiler accepts "np-payment" and rejects "". You can see that T is inferred as the tuple type ["np-payment", ""] and the args argument is of the mapped tuple type [EnforceNonEmptyValue<"np-payment">, EnforceNonEmptyValue<"">].

    Playground link to code