TypeScript Discriminated Union with Optional Discriminant - reactjs

I've created a discriminated union that's later used to type props coming into a React component. A pared down sample case of what I've created looks like this:
type Client = {
kind?: 'client',
fn: (updatedIds: string[]) => void
};
type Server = {
kind: 'server',
fn: (selectionListId: string) => void
};
type Thing = Client | Server;
Note that the discriminant, kind, is optional in one code path, but is defaulted when it is destructured in the component definition:
function MyComponent(props: Thing) {
const {
kind = 'client',
fn
} = props;
if (kind === 'client') {
props.fn(['hey']);
// also an error:
// fn(['hey'])
} else {
props.fn('hi')
// also an error:
// fn('hey')
}
}
What I'm trying to understand is what's going on with this conditional. I understand that the type checker is having trouble properly narrowing the type of Thing, since the default value is separate from the type definition. The oddest part is that in both branches of the conditional it insists that fn is of type (arg0: string[] & string) => void and I don't understand why the type checker is trying to intersect the parameters here.
I would have expected it to be unhappy about non-exhaustiveness of the branches (i.e. not checking the undefined branch) or just an error on the else branch where the 'server' and undefined branches don't line up. But even trying to rearrange the code to be more explicit about each branch doesn't seem to make any impact.
Perhaps because the compiler simply can't narrow the types so tries an intersection so it doesn't matter which path--if the signatures are compatible then it's fine to call the function, otherwise you basically end up with a never (since string[] & string is an impossible type)?
I understand that there are a variety of ways I can resolve this via user-defined type predicates or type assertions, but I'm trying to get a better grasp on what's going on here internally and to find something a bit more elegant.
TS Playground link

It's an implementation detail in TS. Types are not narrowed when you are storing the value in a different variable. The same issue exists for the square bracket notation. You can refer to this question, it deals with a similar issue. Apparently, this is done for compiler performance.
You can fix your issue by using both props.fn and props.kind.
playground
Or write a type guard function.

Related

Flow complains about type incompatibility even though I check type first

I have written a React dropdown component which I can supply either of the following to :
An array of strings
An array of simple JSON object each of which contains two properties of text and icon.
My simple flow types look as follows:
type DropdownMenuItemType = DropdownMenuIconAndTextType | string;
type DropdownMenuIconAndTextType ={
text: string,
icon?: React$Element<React$ElementType>;
}
Previous versions of the component only supports strings. The addition of an element to support text and an icon is a new feature request which I am in the process of implementing. I don't want any breaking changes for my existing users.
Therefore within my component I try to attempt to convert any string supplied and wrap it in a DropdownMenuIconAndTextType so everything ends up as this type. Items that are already DropdownMenuIconAndTextType just remain so.
let Array<DropdownMenuItemType> originalItems =
let Array<DropdownMenuIconAndTextType> convertedItems = [];
{'English', 'French', 'German', {text: 'Dutch', icon : <SomeIcon />}};
items.forEach( (currItem: DropdownMenuItemType) => {
if(typeof currItem === DropdownMenuIconAndTextType){
convertedItems.push(currItem);
}
else {
convertedItems.push({text: currItem.toString()});
}
});
However flow has one error with :
if(typeof currItem === DropdownMenuIconAndTextType){
convertedItems.push(currItem);
}
and it says that currItem could still be a string and is incompatible with convertedItems despite it being type checked as DropdownMenuIconAndTextType.
What do I need to do to satisfy flow in this scenario ? Thanks in advance.
I believe you're mixing up the distinction between Flow's type code and JS code.
Inside of type signatures, typeof returns the type of a literal value, as described here. In the JS code that exists at runtime, such as in your if statement, typeof will just tell you whether something is a string, object, etc., as described here. So the left side of your conditional operator will evaluate to either "string", or "object", not to the actual Flow type of the variable.
On the right side of your conditional, you have the Flow type DropdownMenuIconAndTextType, which only exists at type-checking time, not at runtime. I'm kind of surprised that Flow doesn't give you an error because of that.
Try something like this:
if(typeof currItem !== 'string'){
convertedItems.push(currItem);
}
This will check whether the value that exists at runtime is a string or an object, which should work with Flow's type refinements.

Is there any way to use flow to restrict specific string patterns?

I'm using Flow on a React webapp and I'm currently facing a use-case where I'm asking for the user to input certain time values in a "HH:mm" format. Is there any way to describe what pattern is being followed by the strings?
I've been looking around for a solution but the general consensus which I agree to to a certain point seems to be that you don't need to handle this kind of thing using Flow, favouring using validating functions and relying on the UI code to supply the code following the correct pattern. Still, I was wondering if there is any way to achieve this in order to make the code as descriptive as possible.
You want to create a validator function, but enhanced using Opaque Type Aliases: https://flow.org/en/docs/types/opaque-types/
Or, more specifically, Opaque Type Aliases with Subtyping Constraints: https://flow.org/en/docs/types/opaque-types/#toc-subtyping-constraints
You should write a validator function in the same file where you define the opaque type. It will accept the primitive type as an argument and return a value typed as the opaque type with subtyping constraint.
Now, in a different file, you can type some variables as the opaque type, for example in function arguments. Flow will enforce that you only pass values that go through your validator function, but these could be used just as if they were the primitive type.
Example:
exports.js:
export opaque type ID: string = string;
function validateID(x: string): ID | void {
if ( /* some validity check passes */ ) {
return x;
}
return undefined;
}
import.js:
import type {ID} from './exports';
function formatID(x: ID): string {
return "ID: " + x; // Ok! IDs are strings.
}
function toID(x: string): ID {
return x; // Error: strings are not IDs.
}

Typescript: Type assertion as any, what does this mean?

I am trying to understand how a section of code works. I have seen type assertion before but for some reason I can't wrap my head around this.
(this.whatever as any).something([]);
Larger section of code:
resetThis(): void {
if (this.whatever) {
(this.whatever as any).something([]);
}
}
When trying to run this block I get the error: this.whatever.someting is not a function
Your code is equivalent to this plain JavaScript code:
this.whatever.something([]);
When you say as any in TypeScript, you're telling the compiler to ignore the previous type of the preceding expression and instead treat it as if it were of the any type. So in your case, you're telling the compiler that this.whatever is of type any.
The any type in TypeScript is basically a type that means "we don't know anything about what this variable really is, so let me do whatever I want with it". The TypeScript handbook describes it as "opting out of typechecking".
Let's say this.whatever has type IMyType.
With:
IMyType {
prop1: string;
prop2: string;
}
so if you call this.whatever.something([]), the compiler will scream. Because the something([]) function is not defined in IMyType.
Using as any will tell the compiler that for that specific statement, it should consider this.whatever to be of type any. Which means it can have any property or method it wants.

Typescript Class.prototype.MyFunction makes errors but working

I can't explain why I get an error but code works. Is that compiler bug? (I use Visual Studio Code with Angular 2)
class A
{
fun(a: number)
{
return a+2;
}
}
A.prototype.F = function() { return "F here!"+this.fun(1); } // This makes error: The property 'F' does not exist on value of type 'A'
var a: A = new A();
console.log(a.F());
And bonus: This is not working at all! (no access to this.fun())
A.prototype.F2 = () => { return "F2 here!"+this.fun(1); } // ()=>{} is not working! cause _this is not defined!
...
console.log(a.F2());
Edit #1
As #seangwright said I need to use Module Augmentation but...
As far as it's working with simple example with my A class I can't make it work with Angular's ComponentFixture. This should solve my problem if I do this like in Typescript example:
declare module '#angular/core/testing' // I was trying without this line and with 'global' instead of '#angular/core/testing' but nothing helps
{
interface ComponentFixture<T>
{
TestOf(cssSelector: string): string;
}
}
But I still get an error:
'ComponentFixture' only refers to a type, but is being used as a value
here.'
at this point:
ComponentFixture.prototype.TextOf = function(cssSelector: string): string
{
...
}
There is even more errors, for example when I try to use it:
let fixture: ComponentFixture<EditableValueComponent>;
fixture = TestBed.createComponent(EditableValueComponent);
I got:
'ComponentFixture' is not assignable to type
'ComponentFixture'. Two different types with
this name exist, but they are unrelated. Property 'TestOf' is
missing in type 'ComponentFixture'
So again: Code works but has many compilation errors. Or maybe I'm missing something obvious?
I get the feeling you are a C# developer based on how you format your code.
Part 1
In Typescript once you declare your class, the type system expects it to have the properties (shape) you define and that's it.
The more of the type system you want to use, the less dynamic your objects will/can be.
That said, the reason your code runs (transpiles) correctly is because this is an error in the context of Typescript's structural type system, not Javascript's dynamic type system. So Typescript will tell you A doesn't have a property F at compile time, but Javascript doesn't care that it's added at runtime.
One solution is to merge the class with an interface
class A {
fun(a: number) {
return a + 2;
}
}
interface A {
F(): string;
}
A.prototype.F = function () { return "F here!" + this.fun(1); }
var a: A = new A();
console.log(a.F());
Another would be to temporarily abandon the type system
class A {
fun(a: number) {
return a + 2;
}
}
(A.prototype as any).F = function () { return "F here!" + this.fun(1); }
var a: A = new A();
console.log((a as any).F());
But that becomes verbose and prone to errors and loses the benefits that a type system brings.
You mention you are using Typescript with Angular 2. You could write in ES2015 if you wanted a more dynamic syntax. But then you will lose some of the benefits that Angular 2 gets from using Typescript (better tooling, smaller deployments).
Part 2
The reason your second example doesn't work at all has nothing to do with Typescript and everything to do with Scope (or execution context) in Javascript, specifically ES2015 arrow functions.
An arrow function does not create its own this context, so this has its original meaning from the enclosing context.
Unlike in your first example you are not using the traditional function declaration syntax and instead are using the () => {} arrow function syntax. With your first example
A.prototype.F = function() { return "F here!"+this.fun(1); }
this refers to whatever context F() is going to be executing in. Since you define it on the prototype of A it is going to be executing in the context of A. A has a .fun() method so this.fun() is going to be the same one defined in your class above.
With your second example, F2 is not going to be executing in the context of A despite being defined as a method of its prototype. The arrow function syntax is instead going to allow F2 to run in the context of the enclosing context which is the global window object unless you are running in strict mode in which case
in browsers it's no longer possible to reference the window object through this inside a strict mode function.
So this will be undefined and calling fun() on undefined is going to throw an error.
Try adding a console.log(this) to your F2 function.
A.prototype.F2 = () => { console.log(this); return "F2 here!"+this.fun(1); }
When you run the transpiled Javascript you will probably see Window logged out to the console, and then probably an error like Uncaught TypeError: _this.fun is not a function
Use the Typescript Playground to write some Typescript, see what the tooling tells you, what transpiled Javascript is created and then run it to see if your Javascript is correct.

Iterating over module exports with implicit any

So I'm writing a module with sub-modules for angular. Notation:
module App.services {
export class SomeService { }
}
and I initialize all services using:
function defToArray(def: any): any[] {
return (def.dependencies || []).concat(def)
}
for (var s in App.services)
app.service(s, defToArray(App.services[s]));
However defToArray(App.services[s]) causes "Index signature of object type implicitly has an 'any' type".
I already tried casting like defToArray(<any>App.services[s]) and defToArray(App.services[s] as any), but no luck.
Any thoughts?
Casts are not very tightly binding.
Instead of
defToArray(<any>App.services[s])
try
defToArray((<any>App.services)[s])
or if you prefer
defToArray((<{[key:string]:any}>App.services)[s])
For clarification, the goal here is not to cast the type of App.services[s], you're actually trying to provide type information for the access-by-index operator. This is only an issue when noImplicitAny is enabled (but IMO it always should be, so it's just one of the things you learn to adjust to)

Resources