Reputation: 908
I have a question about typescript optional properties of interfaces. Assuming the following code:
interface Test {
prop1: string;
prop2?: string;
}
function someFunction(data: {prop1: string, prop2: string}) {
console.log(data.prop1 + ": " + data.prop2);
}
function otherFunction(data: Test) {
if (data.prop2) {
someFunction(data); // prop2 might be undefined!
}
}
and having a strict mode set to true.
Typescript gives me the following error:
Argument of type 'Test' is not assignable to parameter of type '{ prop1: string; prop2: string; }'.
Property 'prop2' is optional in type 'Test' but required in type '{ prop1: string; prop2: string; }'.
And the question is: why it is like that? Why doesn't typescript understand this if assertion?
First of all, I'd love to understand why? But also some workaround that does not produce any additional runtime code or some tons of type assertion would be a nice to have if it's possible at all?
Upvotes: 6
Views: 15105
Reputation: 1269
Extending the @TitianCernicova-Dragomir answer with specific checks for undefined
and null
in case you want to distinguish them.
For undefined
guard:
type MakeDefined<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> &
{ [P in K]-?: Exclude<T[P], undefined> };
export function ensureFieldDefined<T, K extends keyof T>(
o: T | MakeDefined<T, K>,
field: K
): o is MakeDefined<T, K> {
return o[field] !== undefined;
}
For null
guard:
type MakeNonNull<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>> &
{ [P in K]: NonNullable<Exclude<T[P], undefined>> };
export function ensureFieldNotNull<T, K extends keyof T>(
o: T | MakeNonNull<T, K>,
field: K
): o is MakeNonNull<T, K> {
return o[field] !== null;
}
Usage
interface Test {
prop1?: string | null;
}
function otherFunction(data: Test) {
if (ensureFieldDefined(data, 'prop1')) {
someFunction(data); // prop1 is now marked as defined (prop1: string | null)
}
if (ensureFieldNotNull(data, 'prop1')) {
someFunction(data); // prop1 is now marked as non-null (prop1?: string)
}
if (ensureFieldDefined(data, 'prop1') && ensureFieldNotNull(data, 'prop1')) {
someFunction(data); // prop1 is now marked as fully mandatory (prop1: string)
}
}
Thanks again @TitianCernicova-Dragomir for awesome trick :)
Upvotes: 0
Reputation: 44316
There is a Required
built in type in Typescript that does exactly what you want:
/** * Make all properties in T required */ type Required<T> = { [P in keyof T]-?: T[P]; };
You can now define a type guard for data is Required<Test>
:
const hasRequiredProp2 = (data: Test): data is Required<Test> => {
return data.hasOwnProperty('prop2');
};
All you need to do is use this test using the type guard like so:
function otherFunction(data: Test) {
if (hasRequiredProp2(data)) {
someFunction(data); // Narrowed to Required<Test>
}
}
Upvotes: 4
Reputation: 1487
If you want a specific type guard instead of the general solution of Titian Cernicova-Dragomir, you can write a simple function.
interface TestWithProp2 {
prop1: string;
prop2: string; // required
}
// Special return type
function isTestWithProp2(x: Test): x is TestWithProp2 {
return x.prop2 !== undefined;
}
// Use
if (isTestWithProp2(data)) {
someFunction(data);
}
Source: Typescript: User defined type guards
Upvotes: 2
Reputation: 249466
Typescript does understand typeguards like you use, the problem is that they only affect the type of the field not the whole object . So for example under strict null checks we would get the following :
function stringNotUndefined(s: string) {}
function otherFunction(data: Test) {
stringNotUndefined(data.prop2) // error
if (data.prop2) {
stringNotUndefined(data.prop2) //ok
someFunction(data); // still error
}
}
We can create a custom type guard that will mark the checked fields as non undefined :
interface Test {
prop1: string;
prop2?: string;
}
function someFunction(data: { prop1: string, prop2: string }) {
console.log(data.prop1 + ": " + data.prop2);
}
type MakeRequired<T,K extends keyof T> = Pick<T, Exclude<keyof T, K>> & {[P in K]-?:Exclude<T[P],undefined> }
function checkFields<T, K extends keyof T>(o: T | MakeRequired<T,K>, ...fields: K[]) : o is MakeRequired<T,K>{
return fields.every(f => !!o[f]);
}
function otherFunction(data: Test) {
if (checkFields(data, 'prop2')) {
someFunction(data); // prop2 is now marked as mandatory, works
}
}
Edit
The above version may have a bit too much overhead for such a simple check. We can create a much simpler version for just one field (and use &&
for more fields). This version has a lot less overhead and might even be inlined if on a hot path.
interface Test {
prop1?: string;
prop2?: string;
}
function someFunction(data: { prop1: string, prop2: string }) {
console.log(data.prop1 + ": " + data.prop2);
}
type MakeRequired<T,K extends keyof T> = Pick<T, Exclude<keyof T, K>> & {[P in K]-?:Exclude<T[P],undefined> }
function checkField<T, K extends keyof T>(o: T | MakeRequired<T,K>,field: K) : o is MakeRequired<T,K>{
return !!o[field]
}
function otherFunction(data: Test) {
if (checkField(data, 'prop2') && checkField(data, 'prop1')) {
someFunction(data); // prop2 is now marked as mandatory, works
}
}
Upvotes: 8
Reputation: 235
Typescript is statically type-checked, so the type conversion from Test
to {prop1:string,prop2:string}
must be valid at compile time. The if
condition is evaluated at run time instead, so it cannot be used for static type-check analysis (at least not in a trivial way).
It may be possible to envisage ways to enrich Typescript so that guards could be used to allow the kind ot type casting you wish to do, but it's simply not the way it is currently designed to work (and it is more complicated than it might at first seem).
To do what you want to do, you could write a helper function that takes a parameter of type Test
and returns one of type {prop1:string,prop2:string}
, by filling the optional parameter with some default value if it doesn't have one in the Test
parameter.
By the way, you may want to look at the discussion in Setting default value for TypeScript object passed as argument
Upvotes: 0