With the following (very contrived) example where:
If the object has a certain value for an attribute, then another (optional) attribute must be defined. I want to catch errors in the constructor so that issues are immediately caught by the user.
Later in a method, I want to rely on the type checking that we performed earlier. Is there a way to "cascade" this check downwards, or leverage the checking we do in the constructor elsewhere in the class? Is there, generally, a more elegant way to structure this type of flow?
interface ExampleOptions {
someType: "basic" | "needsExtra"
extra?: string
}
class Example {
readonly someType: "basic" | "needsExtra";
readonly extra?: string;
constructor(opts: ExampleOptions){
this.someType = opts.someType
this.extra = opts.extra
// handle type checking here to raise error immediately
if (this.someType === "needsExtra" && !this.extra) {
throw new Error("gotta define extra when using type 'needsExtra'")
}
}
doSomething() {
return this.someType === "basic" ?
Example.helper(this.someType) : Example.helperExtra(this.someType, this.extra) // this throws
}
private static helper(someType: string): string {
return someType
}
private static helperExtra(someType: string, extra: string) {
return `${someType}${extra}`
}
}
The doSomething()
method raises an exception:
Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
Type 'undefined' is not assignable to type 'string'.(2345)
even though we know from the constructor that this value will be initialized. I could always just add another check in the method, but that would be redundant. And I believe using the !
(non-null assertion) operator is frowned upon.
CodePudding user response:
Instead of a single type, use a union of two types.
More
Complete simplified example:
type ExampleOptions = {
someType: "basic" | "needsExtra"
extra?: string
}
type Better =
| { someType: 'basic' }
| { someType: 'needsExtra', extra: string }
class Example {
constructor(readonly config: Better) { }
doSomething() {
return this.config.someType === "basic" ?
[this.config.someType] : [this.config.someType, this.config.extra]; // OK!
}
}
CodePudding user response:
The method doSomething() tells me that you squeezed two classes in a single one. The constructor shows the same thing but it also suffers from using the classes as containers for properties, not as real OOP.
My suggestion for cleaner code and real OOP and polymorphism is to declare an interface and implement it in two or more classes that implement the alternatives paths of your current code:
interface Example {
doSomething();
}
class BasicExample implements Example {
// this class does not even need a constructor
public doSomething() {
return 'basic';
}
}
class AdvancedExample implements Example {
public constructor(private extra: string) {
// depending on what you understand by `!this.extra`, this constructor
// might need or might not need to do anything more
// the TypeScript compiler already prevents invoking the constructor
// without a value for parameter `extra`
}
public doSomething() {
return `needsExtra${extra}`;
}
}
It is clear that the sample code that you posted in the question is over-simplified and the real code does more than what I did above.
However, by separating in different classes the things that are different makes the code much cleaner, better organized and easier to read and understand.