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?
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
}
}
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
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>
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With