5

As a follow-up to my previous question...

Is there a way to provide a default value for valueProp in the following example?

type ValueType = 'value' | 'defaultValue'

type Props<T extends ValueType> =
  Record<T, string> 
  & { other: string }

function props<T extends ValueType>(valueProp: T): Props<T> {
  return {
    [valueProp]: 'value',
    other: 'other',
  } as Props<T>
}

let { defaultValue, other } = props('defaultValue') // ok
let { value } = props('value') // ok

Play


When I try this:

function props<T extends ValueType>(valueProp: T = 'value'): Props<T> {
  return {
    [valueProp]: 'value',
    other: 'other',
  } as Props<T>
}

, I get this error:

Type '"value"' is not assignable to type 'T'.
  '"value"' is assignable to the constraint of type 'T', but 'T' could be instantiated with a different subtype of constraint 'ValueType'.

I understand this error (1, 2, 3, 4) in general, but still don't always know the best way to resolve this error.

Is there an easy way to do what I'm trying to do?

It just seems like there ought be an easy way to provide a default value, but maybe default values just don't mix so well with generic type constraints?

Default type for the generic type?

I tried this:

function props<T extends ValueType = 'value'>(valueProp: T = 'value'): Props<T> {
  return {
    [valueProp]: 'value',
    other: 'other',
  } as Props<T>
}

but of course got the same error, because T could still be defaultValue if a specific T is provided:

props<'defaultValue'>()

Type assertions?

I thought of doing this, which compiles, but still doesn't prevent valueProp from disagreeing with T:

function props<T extends ValueType>(valueProp: T = 'value' as T): Props<T> {
  return {
    [valueProp]: 'value',
    other: 'other',
  } as Props<T>
}

console.log(props<'defaultValue'>())
// => {value: "value", other: "other"}

Something more complicated?

I'm still hoping there's a simple solution, but if not, maybe something more complicated would work?

Maybe using a mapping table that maps from a type to the default value for that type?

For inspiration, maybe something kind of like:

Tyler Rick
  • 9,191
  • 6
  • 60
  • 60
  • 1
    Use the overload approach i first suggested in my answer . You can add an overload without the extra parameter and the implementation signature (the last one) can have a value for the parameter since it is not generic – Titian Cernicova-Dragomir Oct 28 '19 at 20:22
  • Okay, I guess overloads have important uses after all! :) Thanks for pointing out the simple solution yet again. – Tyler Rick Oct 28 '19 at 22:30
  • The reason I'm trying to avoid function overloads, by the way, is that this won't be the _only_ dynamic key based on parameters — and I'm afraid if I had to enumerate every possible _combination_ of params using overloads, it could result in a combinatorial explosion. I guess I'll wait to worry about that problem until I actually run into it, though. – Tyler Rick Oct 28 '19 at 22:30

1 Answers1

2

Based on Titian Cernicova-Dragomir's comment, we can do this pretty simply using function overloads:

type ValueType = 'value' | 'defaultValue'

type Props<T extends ValueType> =
  Record<T, string> 
  & { other: string }

function props<T extends ValueType>(valueProp: T): Props<T>
function props(): Props<'value'>
function props(valueProp: ValueType | undefined = 'value') {
  return {
    [valueProp]: 'value',
    other: 'other',
  }
}

Play


Or if we want to move valueProp from positional parameters into pseudo named parameters (using object destructuring):

type PropsOptions<VT extends ValueType | undefined> = {
  valueProp?: VT
}

function props<VT extends ValueType>({valueProp}: PropsOptions<VT>): Props<VT>
function props(): Props<'value'>
function props({ valueProp = 'value' }: PropsOptions<ValueType | undefined> = {}) {
  return {
    [valueProp]: 'value',
    other: 'other',
  }
}

Play

Tyler Rick
  • 9,191
  • 6
  • 60
  • 60