aweiu
aweiu

Reputation: 392

How to make a generic of a class is required?

The class is:

class Test<P> {
  constructor(data: P) {}
}

I hope the following code does not pass the type check because it has no incoming generic:

new Test({ a: 1 })

I know that the generic P above is automatically derived as {a: number}, but this is not what I want, the following is.

new Test< {a: number} >({ a: 1 })

I tried a lot of methods, but in the end the generic P will be automatically derived into the constructor's parameter type.

Upvotes: 6

Views: 3708

Answers (3)

Sarsaparilla
Sarsaparilla

Reputation: 6690

Based on Oleg's answer from https://stackoverflow.com/a/57683742/226844

We want to have the generic parameter default to never when it is not explicitly specified, so that an error is raised. But when we do this:

function foo <T=never> (bar: T) {}

foo("hello")  //doesn't raise error because TypeScript infers T=string from type of 'bar'

The following trick works by preventing TypeScript from being able to infer T from the type of 'bar'.

function foo <T=never, U extends T=T> (bar: U) {}

foo("hello")  //raises error because T=never

Upvotes: 2

Tim
Tim

Reputation: 1286

To update this for TypeScript 3.8.3 you can simplify this:

type NoInfer<T> = [T][T extends unknown ? 0 : never];

// Usage in a class:
class Test<P = "No type parameter was supplied"> {
  constructor(data: NoInfer<P>) {}
}

new Test({ a: " "}) // err Argument of type '{ a: string; }' is not assignable to parameter of type '"No type parameter was supplied"'.
new Test<{ a: string }>({ a: " "})// ok

// Usage in a function:
function Foo<P = "No type parameter was supplied"> (
  data: NoInfer<P>
) {
  return undefined
}

Play

Upvotes: 5

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 250336

There is a question that deals with a very similar issue here:

async function get<U = void>(url: string & (U extends void ? "You must provide a type parameter" : string)): Promise<U> {
    return null as any;
}

The difference is that in that case the type parameter was not used in the parameters at all. This means typescript had no place to infer it the type parameter from. If, as in your case the type parameter is used in the argument list, typescript will use the argument as a source to infer the type parameter from and our trick of using the default value as a signal that no type parameter was explicitly specified will not work (since typescript not use the default if it can infer the type parameter).

The solution is simple enough, let typescript know that we don't want it to infer T from a specific parameter. While there is no builtin support to do this jcalz offers a reasonable workaround here

type NoInfer<T> = [T][T extends any ? 0 : never];
class Test<P = void> {
  constructor(data: NoInfer<P> & (P extends void ? "No type parameter was supplied" : {})) {}
}

new Test({ a: " "}) // err Argument of type '{ a: string; }' is not assignable to parameter of type 'void & "No type parameter was supplied"'.
new Test<{ a: string }>({ a: " "})// ok

play

Upvotes: 8

Related Questions