I have a picked type ColumnSetting
where the original type Column
has almost all properties as optional:
type ColumnSetting = Pick<Column, 'colId' | 'width' | 'sort'>;
type Column = {
colId: string,
width?: number,
sort?: number
something?: string
}
Column
has several more properties than ColumnSetting
. However wherever ColumnSetting
is used as a type the TS engine accepts Column
without complaints. How can I define ColumnSetting
to make the TS engine not allow Column
where ColumnSetting
is expected?
Object types in TypeScript are open in the sense that they extra allow properties not mentioned in the declaration. Indeed this is what makes interfaces and object types extendible. Otherwise inferface Foo {a: string}
and interface Bar extends Foo {b: string}
would put us in the unfortunate circumstances that Bar extends Foo
but Bar
is not a valid Foo
. But {a: string, b: string}
is assignable to {a: string}
.
Sometimes people get confused into thinking that TypeScript types are closed or sealed or exact (as mentioned in microsoft/TypeScript#12936), but the closest thing TypeScript has are excess property checks on object literals, which is more like a linter warning and not a type safety check.
That means Pick<T, K>
is always a supertype of T
, so Column
will be assignable to Pick<Column, K>
no matter what K
is. All Pick
does is say that something must have certain keys, not that it must lack the remainder. If you want to exclude other properties, you must do so explicitly. TypeScript doesn't have a direct way to say "this key must not exist". But you can say that a property is optional and of the impossible never
type. Since you can never find a value of type never
, the only way of satisfying {foo?: never}
is by not having a foo
property. (Well, or you can make it undefined
, assuming you don't have the --exactOptionalPropertyTypes
compiler option enabled.)
So you don't want Pick
... you want something like ExclusivePick
:
type ExclusivePick<T, K extends keyof T> =
{ [P in K]: T[P] } & { [P in Exclude<keyof T, K>]?: never }
That is the same as Pick<T, K>
intersected with an object type that prohibits all the keys of T
excluding those in K
.
Let's test it out:
type ColumnSetting = ExclusivePick<Column, 'colId' | 'width' | 'sort'>;
/* type ColumnSetting = {
colId: string;
width?: number | undefined;
sort?: number | undefined;
} & {
something?: undefined;
}*/
Now if you try to assign a Column
to that, you'll get an error:
function foo(col: Column) {
const columnSetting: ColumnSetting = col; // error!
// Types of property 'something' are incompatible.
// Type 'string' is not assignable to type 'undefined'
}
The compiler is saying that the only value it would accept for the something
property of a ColumnSetting
would be undefined
, not string
, and so Column
is inappropriate.