首页 > 解决方案 > What type should I use for default arguments that depend on a type parameter?

问题描述

I'm working on testing the validation logic for a form that has mostly simple text fields, but has some fields that have other kinds of values or require extra logic to set their value. I have an overloaded helper function that looks something like this:

type FieldSetter<T> = (field: Field, value: T) => void; // reusable utility functions to set field values

// overload for the most common case: value is a string, value and setter are optional
function testFieldValidation(
  field: Field,
  valueToTry?: string,
  setFieldValue?: FieldSetter<string>
): void;

// overload for the less common case: value is not a string, value and setter are required
function testFieldValidation<T>(
  field: Field,
  valueToTry: T,
  setFieldValue: FieldSetter<T>
): void;

// function implementation
function testFieldValidation(
  field: Field,
  valueToTry = '', // what should this type be? (defaults to string, which is wrong)
  setFieldValue = defaultStringSetter // and what should this type be?
): void {/* ... */}

I'm trying to figure out what the type should be for the 2nd and 3rd arguments of testFieldValidation's implementation. Here are some things I've already tried:

// error: '""' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint '{}'
// This is just not how type parameters work, see https://github.com/microsoft/TypeScript/issues/21521
// .. though it'd be nice if TS could determine from the overloads that this is safe
function testFieldValidation<T = string>(
  field: Field,
  valueToTry: T = '',
  setFieldValue: FieldSetter<T> = defaultStringSetter
): void {/* ... */}

// error: this signature is incompatible with the first overload
function testFieldValidation(
  field: Field,
  valueToTry: unknown = '',
  setFieldValue: FieldSetter<unknown> = defaultStringSetter
): void {/* ... */}

I'd prefer not to use any if I can get away with it. Anyone have any ideas?

A relevant GitHub issue, but no solutions are proposed: link

标签: typescriptgenericstypescript-generics

解决方案


Overload implementations are fairly loosely typed compared to the call signatures, in that as long as the implementation parameters and return types are at least as wide as the unions of the types in the call signatures, it should work. So the following should type check:

// function implementation
function testFieldValidation<T>(
  field: Field,
  valueToTry: T | string = '',
  setFieldValue: FieldSetter<T> | FieldSetter<string> = defaultStringSetter
): void {/* ... */ }

Whether or not this meets your needs is another story; inside the implementation the compiler will not have any idea that valueToTry and setFieldValue will be correlated to each other (e.g., if the first is string then the latter is definitely FieldSetter<string>), so you will find yourself either performing redundant checks or type assertions:

setFieldValue(field, valueToTry); // error!
// ----------------> ~~~~~~~~~~
// 'T' could be instantiated with an arbitrary type which could be unrelated to 'string'

setFieldValue(field, valueToTry as T & string); // okay, asserted

Speaking of type assertions, that's always a possibility when you know more about the types of things than the compiler does. So, if you want to just tell the compiler that if the default "" is used then that "" will definitely be a valid T, that could work also:

// function implementation
function testFieldValidation<T extends unknown>(
  field: Field,
  valueToTry = '' as T,
  setFieldValue = defaultStringSetter as FieldSetter<T>
): void {/* ... */ }

That might be easier to use inside the implementation, since the necessary assertions have already been done:

setFieldValue(field, valueToTry); // okay

It's up to you which way if any you'd like to go. Hope that helps; good luck!

Playground link to code


推荐阅读