7
votes

So I am trying to write a function which has a generic which extends a certain object thus constrains it. Next I would like to use this generic together with a definition of a parameter to generate a new "enhanced" parameter. This is all good but as soon as I want to introduce a default value to the parameter TypeScript complains with a message as follow (Some different variations of this in the playground):

Function:

const test1 = <T extends { foo?: string }>(options: T & { bar?: boolean } = {foo: 
''}) => {
    console.log(options);
}

The error:

Type '{ foo: string; }' is not assignable to type 'T & { bar?: boolean; }'. Object literal may only specify known properties, but 'foo' does not exist in type 'T & { bar?: boolean; }'. Did you mean to write 'foo'?

The compiler warns me that I probably wanted to use foo, which I actually did. Is it simply not possible to use a generic in this way or is this a bug in TypeScript?

2

2 Answers

4
votes

Type T at the definition time is unknown, so the compiler throws this error that you cannot initialize something you are unaware of. There are a couple of workarounds I can think of, but not sure how useful they are for your case.

You can leave type T as is, and use union types for the options parameter as follows:

const test1 = <T> (options: T | { bar?: boolean } | { foo?: string } = { foo: '' }) => {
  console.log(options);
};

Another workaround is to use a type assertion and manually tell the compiler that the initializer is of the type it needs to be:

const test2 = <T extends { foo?: string }>(options: T & { bar?: boolean } = { foo: '' } as T & { bar?: boolean }) => {
  console.log(options);
};

But keep in mind that these are just workarounds and whenever you have to use a workaround, it implies a flaw in the design. Maybe you can take a second look at your logic and improve it in order to remove the need for these workarounds. Or maybe, you can skip argument initialization and add options.foo = options.foo || ''; as the first line of code in your function. Just some ideas.

1
votes

The reason why none of the initializations work is that you can't initialize something you don't know. Consider the following call:

test1<{ foo: string, goo: boolean}>();

The generic parameter is valid for the function, but it has an extra property, goo, which is mandatory, but unspecified in your default value for options. This is why the compiler complains about the assignment, you don't know the final shape of T, you only know the minimum requirements for it, so you can't build an object that will be compatible withT

If you are ok with options not having all mandatory properties specified on the generic type parameter, you can use a type assertion

const test1 = <T extends { foo?: string }>(options: T & { bar?: boolean } = <any>{foo: ''}) => {
    console.log(options);
}