简体   繁体   中英

How to type a generic type as a subset of an interface in Typescript?

Is it possible to enforce a generic type ( T here) to be a subset of an interface ? Example:

interface Type {
  a: () => number
  b: () => string
}

const func = <T /* Some condition */>(): T => { return ... }

func<Type>(); // OK
func<{a: () => number>(); // OK
func<{a: () => number, c: any}>(); // Error !

Why do I need this ?

The case below built an interface with the return types of Type . ( TypeReturn will mean something like {a: number, b: string} ).

type TypeReturn<T> = { [key in keyof T]: (ReturnType<Type[key]>)[] }

But typescript complains at Type[key] because he can't ensure that T will be a key of Type .

You could use a recursive generic constraint to simulate what you're looking for, since TypeScript doesn't really have exact types which would prohibit extra properties. Here's how I might do it:

type Exactly<T, U> = T & Record<Exclude<keyof U, keyof T>, never>;
const func = <T extends Exactly<Partial<Type>, T>>(): T => { return null! }

func<Type>(); // OK
func<{ a: () => number }>(); // OK
func<{ a: () => number, c: any }>(); // Error !
//   ~~~~~~~~~~~~~~~~~~~~~~~~~~~
//   Types of property 'c' are incompatible.

This does what you've stated you want. Note that TypeReturn will still fail to compile, though, because even though T extends Exactly<Partial<Type>, T> represents the constraint, the compiler can only reason about concrete values of T that way; it doesn't understand the constraint's implications when T is still generic. You'll need to convince the compiler further, such as using the built-in utility type Extract to ensure that K will be assignable to keyof Type :

type TypeReturn<T extends Exactly<Partial<Type>, T>> = {
    [K in keyof T]: (ReturnType<Type[Extract<K, keyof Type>]>)[] // okay now
}

type A = TypeReturn<Pick<Type, "a">> // type A = {  a: number[]; }
type B = TypeReturn<Pick<Type, "b">> // type B = {  a: string[]; }
type AB = TypeReturn<Type> // type AB = { a: number[]; b: string [] }
type Oops = TypeReturn<{ a: () => number, c: any }>
//                     ~~~~~~~~~~~~~~~~~~~~~~~~~~~
//   Types of property 'c' are incompatible.

Okay, hope that helps; good luck!

Link to code

You really shouldn't do this... But you can partially do this by conditionally requiring a specific string argument. Unfortunately, this makes the errors really bad, forcing you to provide an argument to see the actual error you want to trigger. Playground

interface Type {
  a: () => number
  b: () => string
}

declare function func<T extends Partial<Type>>(
    ...check: [keyof T] extends [keyof Type] ? [] :
        ['T keys are not a subset of Type keys']
): T

func<Type>(); // OK
func<{ a: () => number }>(); // OK
func<{ a: number }>(); // Error, not assignable to Type
func<{ a: () => number, c: any }>(); // Error, extra key
    // An argument for 'check' was not provided.

func<never>('') // Argument of type '""' is not assignable to parameter
    // of type '"T keys are not a subset of Type keys"'.(2345)

// However, this won't prevent the following. This cannot be prevented.
const x = { a() { return 1 }, c: false }
const y: Partial<Type> = x
func<typeof y>()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM