When using Typescript the compiler detects the type error. It supports any
data type but it is not recommended to use because it tells the compiler that it doesn’t have to check the data type of the object. When the object isn’t the defined data type it crashes at runtime. It’s better to define concrete data type as much as possible. If it’s not possible to define concrete possible data types in the function you should use unknown
data type instead of any
.
You can clone my git repository if you want to run it on your pc.
Check the object data type by Type Guards
“in” Type Guards
Type Guard can be used when the object is Object type but if it is unknown data type it might not be an object. Therefore, the compiler says object is of type 'unknown'
. In this case, you need to cast it to any.
const personData: unknown = {
name: "yuto",
age: 30,
};
if ("name" in (personData as any)) {
console.log((personData as any).name);
}
However, it doesn’t make sense to use unknown in this case and eslint shows a warning when any is used. Another way that we may come up with is a combination of instanceof
and in
keyword but it doesn’t work because the compiler still doesn’t know what data type personData
is and it shows an error.
if (personData instanceof Object && "name" in personData) {
// Property 'name' does not exist on type 'never'
console.log(personData.name);
}
instanceof interface
I know you tried to use instanceof
for interface but the compiler shows an error like this below.
if(personData instanceof Person) {}
// 'Person' only refers to a type, but is being used as a value here.ts(2693)
As the error message says interface is a type. instanceof
requires an instance but an interface is just a definition.
User defined Type Guards
User-defined type guards can solve the problem.
export function isPerson(object: unknown): object is Person {
return Object.prototype.hasOwnProperty.call(object, "name")
&& Object.prototype.hasOwnProperty.call(object, "age");
}
if (isPerson(data)) {
console.log(`name: ${data.name}, age ${data.age}`);
}
The point here is to call hasOwnProperty
function via call
function because we don’t know whether the object argument is object type or not.
Failed trial by Abstract class
The following code is another trial but it didn’t work because data is just an object. It’s not a class.
export abstract class ManyArgs {
public arg1: string = "";
public arg2: string = "";
public arg3: string = "";
}
console.log("---- Abstract ----")
const data = {
args1: "str 1",
args2: "str 2",
args3: "str 3",
};
if (data instanceof ManyArgs) {
console.log(`${data.arg1}, ${data.arg2}, ${data.arg3}`)
} else {
console.log("instanceof doesn't work.")
}
// result
// ---- Abstract ----
// instanceof doesn't work.
I tried to create a generic function for the check but it was impossible to create it because I couldn’t find a way to get a property list from interface. Object.keys()
function requires object data type and we cannot pass interface there.
Check if the variable is NodeJS.ErrnoException type
Let’s check an actual example. fs.promises.readFile
function throws NodeJS.ErrnoException
. The data type of error variable used in catch is unknown from TypeScript version 4.4. The source is here. We can turn off useUnknownInCatchVariables or add as any
to cast it but let’s try to use a user-defined type guard here.
The type guard function looks like this. There are other properties available but I think these two are enough.
export function isErrnoException(object: unknown): object is NodeJS.ErrnoException {
return Object.prototype.hasOwnProperty.call(object, "code")
|| Object.prototype.hasOwnProperty.call(object, "errno");
}
Let’s try to read a file. If the file doesn’t exist, it throws an error. Without this type check, the compiler shows errors because those properties don’t exist on type “unknown”.
async function runExample() {
try {
await fs.promises.readFile("/not-exist-file");
} catch (e) {
if (isErrnoException(e)) {
console.log(`e.code: ${e.code}`);
console.log(`e.errno: ${e.errno}`);
console.log(`e.message: ${e.message}`);
console.log(`e.name: ${e.name}`);
console.log(`e.path: ${e.path}`);
console.log(`e.stack: ${e.stack}`);
console.log(`e.syscall: ${e.syscall}`);
} else {
console.log(e);
}
}
}
runExample()
.then(() => console.log("done"))
.catch(() => console.log("error"));
// e.code: ENOENT
// e.errno: -4058
// e.message: ENOENT: no such file or directory, open 'C:\not-exist-file'
// e.name: Error
// e.path: C:\not-exist-file
// e.stack: Error: ENOENT: no such file or directory, open 'C:\not-exist-file'
// e.syscall: open
Strict Object Type Check
The way above checks only the property’s existence. If two interfaces have the same properties but one of the data types is different, we somehow need to differentiate them. In this case, typeof
needs to be used to check the data type. However, if the data type is unknown type, we can’t access the property. The following post shows how to solve the problem.
Conclusion
If an interface has a lot of properties it may be cumbersome to create a check function but there is no workaround. Let’s consider a better structure in this case. We may be able to separate the interface if it has many properties.
If you know better solutions, please leave comments.
Do you want to learn more? The following posts may be helpful.
Comments
Why would you write an article to explain in detail how to incorrectly do something??
Do you mean “Failed trial by Abstract class”?
Because someone might try to do a similar thing. If I don’t like the popular solution that we can easily find in StackOverflow or somewhere else, I try to do different things.
If it doesn’t work but looks correct, it takes me a while to know it can’t work because those trials are generally not written.
This failed trial could save time.
A beginner could also learn a new thing and the difference between the correct one and the incorrect one.
Don’t be rude.
Typescript interface are just bad, it’s not his fault.
They should do something about it.
// ------
// strong implementation
// ------
interface IPersonData {
name: string;
age: number;
}
const personData: IPersonData = {
name: "yuto",
age: 30
};
let tmp: IPersonData = personData;
// ------
// extended interface example
// ------
interface IPersonData {
name: string;
age: number;
}
class PersonData implements IPersonData {
age: number;
name: string;
gender: string; // additional field
}
const personData: PersonData = {
name: "yuto",
age: 30,
gender: 'MAN'
};
let tmp: IPersonData = personData;
// ------
// You can also define and use your check method
function checkImplements(x: T) { }
// ...
checkImplements(personData);