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.