Reputation: 153
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
Upvotes: 2
Views: 2345
Reputation: 744
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
Upvotes: 1
Reputation: 329838
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!
Upvotes: 0
Reputation: 26438
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"'.
Upvotes: 2
Reputation: 250176
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.
Upvotes: 1