简体   繁体   中英

Typescript: Declare function that takes parameter of exact interface type, and not one of its derived types

interface I {
    a: number;
}

interface II extends I {
    b: number;
}

function f(arg: I) : void {
    // do something with arg without trimming the extra properties (logical error)
    console.log(arg);
}

const obj: II = { a:4, b:3 };
f(obj);

What I want to do is to make function f to accept only objects of type I and not type II or any other derived interface

Difficult because of the way typescript works. What you can do is add a type field to the base, which a derived interface would override. Then to limit a function to only accept the base explicitly:

interface IFoo<T extends string = "foo"> {
  type: T;
}

interface IBar extends IFoo<"bar"> {
}

function ray(baseOnly: IFoo<"foo">) {
}

let foo: IFoo = { type: "foo" };
let bar: IBar = { type: "bar" };

ray(foo); // OK!
ray(bar); // error

and the output error:

[ts]
Argument of type 'IBar' is not assignable to parameter of type 'IFoo<"foo">'.
  Types of property 'type' are incompatible.
    Type '"bar"' is not assignable to type '"foo"'.

You cannot achieve this in Typescript, in general, in most languages you cannot make such a constraint. One principle of object oriented programming is that you can pass a derived class where a base class is expected. You can perform a runtime check and if you find members that you don't expect, you can throw an error. But the compiler will not help you achieve this.

Another possibility is to give up on interfaces and use classes with private properties and private constructors. These discourage extension:

export class I {
  private clazz: 'I'; // private field
  private constructor(public a: number) { 
    Object.seal(this); // if you really don't want extra properties at runtime
  }
  public static make(a: number): I {
    return new I(a); // can only call new inside the class
  }
}

let i = I.make(3);
f(i); // okay

You can't create an I as an object literal:

i = { a: 2 }; // error, isn't an I
f({a: 2}); // error, isn't an I

You can't subclass it:

class II extends I { // error, I has a private constructor
  b: number;
}

You can extend it via interface:

interface III extends I {
  b: number;
}
declare let iii: III;

and you can call the function on the extended interface

f(iii); 

but you still can't create one with an object literal

iii = { a: 1, b: 2 }; // error

or with destructuring (which creates a new object also),

iii = { ...I.make(1), b: 2 };

, so this is at least somewhat safer than using interfaces.


There are ways around this for crafty developers. You can get TypeScript to make a subclass via Object.assign() , but if you use Object.seal() in the constructor of I you can at least get an error at runtime:

iii = Object.assign(i, { b: 17 }); // no error at compile time, error at runtime

And you can always silence the type system with any , (although again, you can use an instanceof guard inside f() to cause an error at runtime).

iii = { a: 1, b: 2 } as any; // no error
f(iii); // no error at compile time, maybe error if f() uses instanceof

Hope that helps; good luck!

This works for me (on ts 3.3 anyway):

// Checks that B is a subset of A (no extra properties)
type Subset<A extends {}, B extends {}> = {
   [P in keyof B]: P extends keyof A ? (B[P] extends A[P] | undefined ? A[P] : never) : never;
}

// Type for function arguments
type Strict<A extends {}, B extends {}> = Subset<A, B> & Subset<B, A>;
// E.g.
type BaseOptions = { a: string, b: number }
const strict = <T extends Strict<BaseOptions, T>>(options: T) => { }
strict({ a: "hi", b: 4 })        //Fine
strict({ a: 5, b: 4 })           //Error
strict({ a: "o", b: "hello" })   //Error
strict({ a: "o" })               //Error
strict({ b: 4 })                 //Error
strict({ a: "o", b: 4, c: 5 })   //Error

// Type for variable declarations
type Exact<A extends {}> = Subset<A, A>;
// E.g.
const options0: Exact<BaseOptions> = { a: "hi", b: 4 }        //Fine
const options1: Exact<BaseOptions> = { a: 5, b: 4 }           //Error
const options2: Exact<BaseOptions> = { a: "o", b: "hello" }   //Error
const options3: Exact<BaseOptions> = { a: "o" }               //Error
const options4: Exact<BaseOptions> = { b: 4 }                 //Error
const options5: Exact<BaseOptions> = { a: "o", b: 4, c: 5 }   //Error

// Beware of using Exact for arguments:
// For inline arguments it seems to work correctly:
exact({ a: "o", b: 4, c: 5 })   //Error
strict({ a: "o", b: 4, c: 5 })   //Error
// But it doesn't work for arguments coming from variables:
const options6 = { a: "o", b: 4, c: 5 }
exact(options6) // Fine -- Should be error
strict(options6)  //Error -- Is correctly error

You can see more detail in my comment here .

So applied to your example:

interface I { a: number; }

interface II extends I { b: number; }

function f<T extends Strict<I, T>>(arg: T): void {
   // do something with arg without trimming the extra properties (logical error)
   console.log(arg);
}
const obj1: I = { a: 4 };
const obj2: II = { a: 4, b: 3 };
f(obj1); // Fine
f(obj2); // Error

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