Does Typescript support "subset types"? - interface

Let's say I have an interface:
interface IUser {
email: string;
id: number;
phone: string;
};
Then I have a function that expects a subset (or complete match) of that type. Maybe it will pass an entire object, maybe it will just pass in {email: "t#g.com"}. I want the type checker to allow for both.
Example:
function updateUser(user: IUser) {
// Update a "subset" of user attributes:
$http.put("/users/update", user);
}
Does Typescript support this sort of behavior yet? I could find it very useful, particularly with paradigms like Redux.
To clarify, the goal is:
Avoid re-writing an interface and manually setting all attributes to optional.
Avoid assignment of unexpected attributes (such as spelling mistakes).
Avoid imperative logic such as if statements, which forfeit benefits of compile time type checking.
UPDATE: Typescript has announced support for mapped types which should solve this problem once published.

It's worth noting that Partial<T>, as suggested in the accepted answer, makes all fields optional, which is not necessarily what you need.
If you want to make some fields required (e.g. id and email), you need to combine it with Pick:
type UserWithOptionalPhone = Pick<IUser, 'id' | 'email'> & Partial<IUser>
Some explanation:
What Pick does is that it lets you specify a subset of the interface succinctly (without creating a whole new interface repeating the field types, as suggested by other answers), and then lets you use those, and only those fields.
function hello1(user: Pick<IUser, 'id' | 'email'>) {
}
hello1({email: '#', id: 1}); //OK
hello1({email: '#'}); //Not OK, id missing
hello1({email: '#', id: 1, phone: '123'}); //Not OK, phone not allowed
Now, this is not exactly what we need, as we want to allow, but not require phone. To do that, we "merge" the partial and the "picked" version of our type by creating an intersection type, which then will have id and email as required fields, and everything else as optional – exactly how we wanted it.
function hello2(user: Pick<IUser, 'id' | 'email'> & Partial<IUser>) {
}
hello2({email: '#', id: 1}); //OK
hello2({email: '#', id: 1, phone: '123'}); //OK
hello2({email: '#'}); //Not OK, id missing

Typescript now supports partial types.
The correct way to create a partial type is:
type PartialUser = Partial<IUser>;

What you want is this
type Subset<T extends U, U> = U;
this makes sure, that U is a subset of T and returns U as a new type. for example:
interface Foo {
name: string;
age: number;
}
type Bar = Subset<Foo, {
name: string;
}>;
you can not add new properties to Bar which are not part of Foo - and you can not alter types in a non-compatible way. this also works recursively on nested objects.

proper solution with mapped types:
updateUser<K extends keyof IUser>(userData: {[P in K]: IUser[P]}) {
...
}

You can declare some or all fields as optional fields.
interface IUser {
email: string; // not optional
id?: number; // optional
phone?: string; // optional
};

You can seperate it into different interfaces:
interface IUser {
id: number;
};
interface IUserEmail extends IUser {
email: string;
}
interface IUserPhone extends IUser {
phone: string;
}
Have your method receive the base IUser interface and then check for the fields you need:
function doit(user: IUser) {
if (user.email) {
} else if (user.phone) {
}
}

If I understand this question correctly, you want something like Flow's $Shape
So, in one place, you may have something that requires the type
interface IUser {
email: string;
id: number;
phone: string;
};
Then, in another place you want a the type with the same type as IUser just with all the fields now optional.
interface IUserOptional {
email?: string;
id?: number;
phone?: string;
};
You want a way to auto-generate IUserOptional based on IUser without having to write out the types again.
Now, I don't think this is possible in Typescript. Things may change in 2.0, but I don't think we're even close to something like this in Typescript yet.
You could look at a pre-compiler which would generate such code for you before typescript runs, but that doesn't sound like a trivial thing to do.
With this problem in mind, I can only suggest you try Flow instead. In flow you can just do $Shape<IUser> to generate the type you want programmatically. Of course, Flow differs from Typescript in many big and small ways, so keep that in mind. Flow is not a compiler, so you won't get things like Enums and class implementing interfactes

Related

yup - is there any way to set the default value for a string field to be something without defining it for each one

I want that every time I use yup.string(), it will add a specific default value for it
for example:
const schema = yup.object({
text: yup.string()// I want it to also do .default('some string') in the background,
});
or - another option - is there any way to set the default value after creating the scheme? something like setDefault('text', 'some string')
The closest solution I came across to solve your issue is extending your string with a custom method that implements your needs. To do that you need to use addMethod from yup:
import { addMethod, string } from 'yup';
addMethod(string, 'append', function append(appendStr) {
return this.transform((value) => `${value}${appendStr}`);
});
Now, you can use your custom method (append) and apply it to any string you want:
string().append('~~~~').cast('hi'); // 'hi~~~~'
If you want to add the custom method to all your schema types like date, number, etc..., you need to extend the abstract base class Schema:
import { addMethod, Schema } from 'yup';
addMethod(Schema, 'myCustomMethod', ...)
Extra
For Typescript
In your type definition file, you need to declare module yup with your custom method's arguments and return types:
// globals.d.ts
import { StringSchema } from "yup";
declare module 'yup' {
interface StringSchema<TType, TContext, TDefault, TFlags> {
append(appendStr: string): this;
}
}
Unknow behavior for transform method
While I was trying to extend the functionality of the date schema with a custom method that transform the date that user enters from DD-MM-YYY to YYYY-MM-DD, the custom method broke after I used it with other methods like min, max for example.
// `dayMonthYear` should transform "31-12-2022"
// to "2022-12-31" but for some reason it kept
// ignoring the `cast` date and tried to transform
// `1900` instead!
Yup.date().dayMonthYear().min(1900).max(2100).required().cast("31-12-2022") // error
To work around this issue, I appended my custom method at the end of my schema chain:
Yup.date().min(1900).max(2100).required().cast("31-12-2022").dayMonthYear() // works as expected
This issue is mentioned in this GH ticket which I recommend going through it as it's going more in-depth on how to add custom methods with Typescript.
References
addMethod
Extending built-in schema with new methods
Example of addMethod in Typescript (GH ticket)

Rescript Record: Key as Array

In Rescript, one can define a Record in this format:
type record1 = {
a : String
}
but NOT:
type record2 = {
[a] : String
}
I am looking to write a record that compiles to JS like:
{
[Op.or]: [12,13]
}
The use case above comes from Sequelize, and the reference is here.
My current solution:
%raw(`{[Op.or]:[12,13]}`)
It's not entirely clear how you intend to interface with the Op construct, whether you can bind to it or not, but here's an example that does, and along with Js.Dict.t effectively produces the same output:
module Op = {
#val external or: string = "Op.or"
}
Js.Dict.fromList(list{
(Op.or, [12, 23])
})
It does not directly compile to the JS you want, however, which might be a problem if you rely on something that actually parses the source code. But short of that, I believe this should do what you ask for.

AWS-CDK Appsync Codefirst input types

To avoid duplication of data structures I wanted to reuse a type definition on an input type like this
export const DeviceStatus = new ObjectType('DeviceStatus', {
definition: {
time: timestamp,
firmwareVersion: string
},
});
export const DeviceStatusInput = new InputType('DeviceStatusInput', {
definition: {
tenantId: id_required,
deviceId: id_required,
// Reuse of DeviceStatus Field definition
status: DeviceStatus.attribute()
}
});
There is no error since the return type of DeviceStatus.attribute() is fine, and this works for ObjectType inheritance.
From my perspective this should work, but deploying results in a nasty "Internal Error creating Schema" error.
Of course I could move the whole definition into an object and reuse it but that seems weird. Is there any good solution on this for the CodeFirst approach
It seem to be invalid to reference object type in input type.
I recommend to view Can you make a graphql type both an input and output type?
Probably best you can do is to create some convenience method which will create you both object and input type from single definition.

Using State with HTMLElement with createSlice from Redux Toolkit

When trying to create a reducer whose state contains a key that is of type HTMLDivElement (or any other HTMLElement derivative), I get an Argument of type '...' is not assignable to parameter of type for what seems to be all of the keys of the HTMLElement.
interface ITestEvent {
name: string;
id: string;
ref: HTMLDivElement;
}
interface AddEventPayload {
event: ITestEvent;
}
interface TestEventState {
events: ITestEvent[];
}
const initialState: TestEventState = {
events: [],
};
const testSlice = createSlice({
name: 'test',
initialState,
reducers: {
addEvent(state, action: PayloadAction<AddEventPayload>) {
state.events.push(action.payload.event); // Error here
},
},
});
Below is a typescript playground link illustrating the issue.
https://typescriptlang.org/play link here
The same happens when creating a reducer via createReducer.
Is this a limitation of TypeScript in some way/is this expected?
This looks to be an issue with the immer type Draft.
Draft<typeof initialState> seems to end up with something slightly different than the original Event type in there.
But generally, this is advised against and will probably also lead to runtime issues and maybe even the redux devtools crashing when they try to display that event.
It is only ever advised to put serializable normal objects into the state:
https://redux.js.org/style-guide/style-guide/#do-not-put-non-serializable-values-in-state-or-actions

Implementing TypeScript interface with bare function signature plus other fields

How do I write a class that implements this TypeScript interface (and keeps the TypeScript compiler happy):
interface MyInterface {
(): string;
text2(content: string);
}
I saw this related answer:
How to make a class implement a call signature in Typescript?
But that only works if the interface only has the bare function signature. It doesn't work if you have additional members (such as function text2) to be implemented.
A class cannot implement everything that is available in a typescript interface. Two prime examples are callable signatures and index operations e.g. : Implement an indexible interface
The reason is that an interface is primarily designed to describe anything that JavaScript objects can do. Therefore it needs to be really robust. A TypeScript class however is designed to represent specifically the prototype inheritance in a more OO conventional / easy to understand / easy to type way.
You can still create an object that follows that interface:
interface MyInterface {
(): string;
text2(content: string);
}
var MyType = ((): MyInterface=>{
var x:any = function():string { // Notice the any
return "Some string"; // Dummy implementation
}
x.text2 = function(content:string){
console.log(content); // Dummy implementation
}
return x;
}
);
There's an easy and type-safe way to do this with ES6's Object.assign:
const foo: MyInterface = Object.assign(
// Callable signature implementation
() => 'hi',
{
// Additional properties
text2(content) { /* ... */ }
}
)
Intersection types, which I don't think were available in TypeScript when this question was originally asked and answered, are the secret sauce to getting the typing right.
Here's an elaboration on the accepted answer.
As far as I know, the only way to implement a call-signature is to use a function/method. To implement the remaining members, just define them on this function. This might seem strange to developers coming from C# or Java, but I think it's normal in JavaScript.
In JavaScript, this would be simple because you can just define the function and then add the members. However, TypeScript's type system doesn't allow this because, in this example, Function doesn't define a text2 member.
So to achieve the result you want, you need to bypass the type system while you define the members on the function, and then you can cast the result to the interface type:
//A closure is used here to encapsulate the temporary untyped variable, "result".
var implementation = (() => {
//"any" type specified to bypass type system for next statement.
//Defines the implementation of the call signature.
var result: any = () => "Hello";
//Defines the implementation of the other member.
result.text2 = (content: string) => { };
//Converts the temporary variable to the interface type.
return <MyInterface>result;
})(); //Invokes the closure to produce the implementation
Note that you don't need to use a closure. You could just declare your temporary variable in the same scope as the resulting interface implementation. Another option is to name the closure function to improve readability.
Here's what I think is a more realistic example:
interface TextRetriever {
(): string;
Replace(text: string);
}
function makeInMemoryTextRetriever(initialText: string) {
var currentText = initialText;
var instance: any = () => currentText;
instance.Replace = (newText: string) => currentText = newText;
return <TextRetriever>instance;
}
var inMemoryTextRetriever = makeInMemoryTextRetriever("Hello");