i have the following interface
export interface AuctionMOLQueryParams {
filterAuctionDirection?: string;
}
because filterAuctionDirection can be one of three possible values i setted to be enum
export enum ProductDirection {
upwards = 'A01',
downwards = 'A02',
all = 'All'
}
export interface AuctionMOLQueryParams {
filterAuctionDirection?: ProductDirection;
}
the problem is that now when i try to use this object type
let queryParams: AuctionMOLQueryParams = {
filterAuctionDirection: ProductDirection.all,
}
Types of property 'filterAuctionDirection' are incompatible.
Type 'string' is not assignable to type 'ProductDirection'strong text
how can i solve this ? How can i map my filterAuctionDirection to be one of this three values and the type safey will still work correctly ?
TS2717: Subsequent property declarations must have the same type.
Property 'filterAuctionDirection' must be of type 'string', but here has type 'ProductDirection'.
If I understood your question correctly, you're defining the interface AuctionMOLQueryParams twice.
Initially you define the field filterAuctionDirection as string but later you're trying to mark it as ProductDirection.
If you're trying narrow a variable type of an externally declared interface (in this example AuctionMOLQueryParams) you have to define a new interface extending of it.
Example for your use case:
export interface AuctionMOLQueryParams {
filterAuctionDirection?: string;
}
export enum ProductDirection {
upwards = 'A01',
downwards = 'A02',
all = 'All'
}
export interface MyAuctionMOLQueryParams extends AuctionMOLQueryParams {
filterAuctionDirection?: ProductDirection;
}
let queryParamsValid1: MyAuctionMOLQueryParams = {
filterAuctionDirection: ProductDirection.all,
}
let queryParamsInvalid2: MyAuctionMOLQueryParams = {
filterAuctionDirection: "My",
}
let queryParamsInvalid1: MyAuctionMOLQueryParams = {
filterAuctionDirection: "All",
}
Related
I'm trying to write a TS function which gets a nested value from given object. That object can be one of several types so I'm using generics. However, TS complains so I feel like I'm misunderstanding how generics work in TS:
interface BaseValueObject<T> {
value: T | null
}
type StringValue = BaseValueObject<string>
type NumberValue = BaseValueObject<number>
interface FormAData {
name: StringValue,
age: NumberValue
}
interface FormBData {
height: NumberValue
nickname: StringValue
}
interface FormA {
data: FormAData
}
interface FormB {
data: FormBData
}
type Form = FormA | FormB
const getFormValue =
<F extends Form, P extends keyof F['data']>(form: F, property: P) =>
form['data'][property]['value'] // Type 'P' cannot be used to index type 'FormAData | FormBData'
Desired usage:
const formARecord: FormA = {
data: {
name: {
value: 'Joe'
},
age: {
value: 50
}
}
}
const joesAge = getFormValue(formARecord, 'age')
console.log(joesAge) // 50
Playground
Solution
Here's what I ended up doing, similar to what #jered suggested in his answer:
playground
Basically the lesson is to make explicit any invariants you have your typings. In my case, I wasn't formally telling the compiler that every property FormAData and FormBData adhered to the same interface. I was able to do so by extending them from this base interface:
...
type Value = StringValue | NumberValue
interface BaseFormData {
[property: string]: Value
}
interface FormAData extends BaseFormData {
...
You should extend your declaration of generics to the "form" interfaces themselves.
In this case you need to give TypeScript a way to "infer" what the type of the data property of the form will be, in order for property to properly index it.
The way you have it written currently gives an error because you can't use keyof to extract the properties of a union type. Consider this example:
type Foo = {
fooProperty: string;
}
type Bar = {
barProperty: string;
}
type Baz = Foo | Bar;
type Qux = keyof Baz; // type Qux = never
What type is Qux supposed to be? It can't be the keys of two different types simultaneously, so it ends up being of type never which is not very useful for indexing properties of an object.
Instead, consider if your base Form type was itself a generic, that you know should always have a data property, but which the specific type of that data property is unknown until it is actually utilized. Fortunately, you could still constrain some aspects of data to ensure enforcements of its structure across your app:
interface FormDataType {
[key: string]: StringValue | NumberValue;
};
interface Form<T extends FormDataType> {
data: T
};
Then when you write your flavors of Form that have more specific type definitions for the data property, you can do so like this:
type FormA = Form<{
name: StringValue,
age: NumberValue
}>;
type FormB = Form<{
height: NumberValue
nickname: StringValue
}>;
In a way this is sort of like "extending" the type, but in a way that allows TypeScript to use the Form generic to infer (literally) the type of data later on.
Now we can rewrite the getFormValue() function's generic types to match the Form generics in the function signature. Ideally the return type of the function would be perfectly inferred just from the function parameters and function body, but in this case I didn't find a good way to structure the generics so that everything was seamlessly inferred. Instead, we can directly cast the return type of the function. This has the benefit of 1. still checking that form["data"] exists and matches the FormDataType structure we established earlier, and 2. inferring the actual type of the value returned from calling getFormValue(), increasing your overall type checking confidence.
const getFormValue = <
F extends Form<FormDataType>,
P extends keyof F["data"]
>(
form: F,
property: P
) => {
return form["data"][property].value as F["data"][P]["value"];
}
Playground
Edit: on further reflection the generics of the Form interface itself is not really necessary, you could do something else like declare a basic Form interface and then extend it with each specific form:
interface FormDataType {
[key: string]: StringValue | NumberValue;
}
interface Form {
data: FormDataType
};
interface FormA extends Form {
data: {
name: StringValue;
age: NumberValue;
}
};
interface FormB extends Form {
data: {
height: NumberValue;
nickname: StringValue;
}
};
const getFormValue = <
F extends Form,
P extends keyof F["data"]
>(
form: F,
property: P
) => {
return form["data"][property].value as F["data"][P]["value"];
}
I'd like to define the following:
export interface IValidationContextIndex {
[validationContextKey: string]: ValidationContext;
getValidationContexts(): Array<ValidationContext>;
}
This way I can get an object that adheres to the IValidationContextIndex interface and call getValidationContexts on it.
However VSCode is NOT HAPPY with this type of definition. Is it possible to do this in an interface or do I need something like:
class cache {
constructor(public index: IValidationContextIndex ) {};
getValidationContexts() {return Object.value(<any> index)}
}
The problem is that typescripts would prefer that the index signature be consistent will ALL declared members. Since you can use the index to access the getValidationContexts member as well and it's type will not be ValidationContext.
You can make the index signature consistent will the defined members, but it's not ideal as you will need to narrow the return type:
export interface IValidationContextIndex {
[validationContextKey: string]: ValidationContext | (() => Array<ValidationContext>);
getValidationContexts(): Array<ValidationContext>;
}
A way to get around this restriction is to use an intersection type:
type IValidationContextIndex = {
[validationContextKey: string]: ValidationContext;
} & {
getValidationContexts(): Array<ValidationContext>;
}
But objects of this type can't be created directly, for the same index incompatibility reason. You would need to create the objects using Object.assign :
let o : IValidationContextIndex = Object.assign({ a: new ValidationContext() }, {
getValidationContexts(): Array<ValidationContext> {
return Object.values(this)as any
}
});
Or using a type assertion:
let o : IValidationContextIndex = {
getValidationContexts(): Array<ValidationContext> {
return Object.values(this)as any
}
} as IValidationContextIndex;
In React I can restrict a variable to subset of values, like
PropTypes.oneOf(['Home', 'About']),
How do I do that in TypeScript?
PS: I am not using TypeScript with React.
You can combine static strings (or any regular type) by defining a union type:
type SomeType = 'Home' | 'About';
Or within an interface:
interface SomeType {
prop : 'Home' | 'About';
}
And of course you can combine other types as well:
type SomeType = string | boolean;
You can use enum.
Enums allow us to define a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases.
enum vs union-type
Union types are a compile time concept
Enums are real objects that exist at runtime
You can iterate over an enum
... see this question
Example with enums:
enum PostStatus {
DRAFT = "DRAFT",
READY = "READY",
PUBLISHED = "PUBLISHED",
}
class Post {
constructor(private status: PostStatus) {
this.status = status;
}
}
const myPost = new Post(PostStatus.DRAFT);
console.log(myPost);
function doStuff(postStatus: PostStatus) {
switch (postStatus) {
case PostStatus.DRAFT:
console.log('Still working on it');
break;
case PostStatus.PUBLISHED:
console.log('Done.');
break;
default:
console.log('Other ...');
}
}
Example with union type:
type PostStatus = "DRAFT" | "READY" | "PUBLISHED";
class Post {
constructor(private status: PostStatus) {
this.status = status;
}
}
const myPost = new Post("DRAFT");
console.log(myPost);
function doStuff(postStatus: PostStatus) {
switch (postStatus) {
case "DRAFT":
console.log('Still working on it');
break;
case "PUBLISHED":
console.log('Done.');
break;
default:
console.log('Other ...');
}
}
Only one type among several one is a union type, and in your case a union of string literal.
You can convert an array of string literals into a union of string literals as follow:
If you do have a const array or string you can define a type:
const menuList = ["Home", "About"] as const;
type menuName = typeof menuList[number] // "Home" | "About"
If you do already have a type with the array just do:
type menuList = ["Home", "About"];
type menuItem = menuList[number] // "Home" | "About"
Here is a solution using an enum type, react proptypes and typescript props
export enum AppPage {
Home = 'Home',
About = 'About',
}
export MyComponentProps = {
page: AppPage
}
export MyComponentPropTypes = {
page: PropTypes.oneOf<AppPage>(Object.values(AppPage)).isRequired
}
export MyComponent = (props:MyComponentProps) => {
return <div></div>
}
MyComponent.propTypes = MyComponentPropTypes
I know some of typescript's advantages are enabling type-safe functions -
but is it possible to ensure my function can only get objects with specific keys, or in other words - objects of specific structure ?
I know of many elegant ways to test if a nested key exists, such as [this one][1] ,
and I can of course run a small check at the beginning of my function - but the reason I'm asking this is because my function will be used by many other programmers - and I want to ensure they can understand what input they should insert just from the function's signature.
Example:
function printName(user){
console.log(user.details.name); // I want to ensure details.name exist
}
and I would wish to have some feature like:
function (user : {details: {name : string}}){
//same ...
}
[1]: https://stackoverflow.com/questions/2631001/javascript-test-for-existence-of-nested-object-key#answer-4034468 "this one"
interface User {
details:{name: string}
}
function printName(user:User){
console.log(user.details.name); // I want to ensure details.name exist
}
This is exactly the feature you desire:
function printName(user: { [key: string]: any, details: { [key: string]: any, name: string }}) {
console.log(user.details.name)
}
Allow any properties and require details + name.
More legible and protected against unintentional changes:
// oftentimes you put the following interfaces in extra files:
interface Details {
readonly [key: string]: any // = allow other properties than "name"*
readonly name: string
}
interface User {
readonly [key: string]: any // = allow other properties than "details"*
readonly details: Details
}
// *) consider explicitly specifying them
function printName(user: User) {
console.log(user.details.name)
}
And use the following code if you know other developers may call your function from plain JavaScript code (not TypeScript code):
function printName(user: User) {
const name = ((user || <User>{}).details || <Details>{}).name
if(name == void 0) {
console.error("user name missing")
}
console.log(name)
}
Code on TypeScript Playground
Use keyof if you want to retrieve a value from a specific property, this will highlight the incorrect property name.
You can also use Object.keys to check if the property exists.
interface UserProps {
name: string;
age: number;
}
export class User {
constructor(private data: UserProps) {}
get(propName: keyof UserProps): string | number {
const dataKeys = Object.keys(this.data);
if (!dataKeys.includes(propName)) {
throw Error(`Property ${propName} does not exists on user object`);
}
return this.data[propName];
}
}
You can use interfaces in typescript
export interface Details{
name:string,
age: number
}
export interface User{
details : {
[key: string]: Details
};
}
function printName(user : User){}
I'm having a strange problem with typescript interfaces. Because I'm using mongoose models I need to define one, but for some reason it's not recognising things that I have explicitly imported. This part works fine:
export interface ITrip extends mongoose.Document {
//
}
export var TripSchema = new mongoose.Schema({
//
});
export var Trip = mongoose.model<ITrip>('Trip', TripSchema);
Now, I'm defining another interface, that has an array of Trip. I need this for subdocuments.
import {Trip, ITrip} from '../trips/trip.model';
export interface IFeed extends mongoose.Document {
lastSnapshot: {
trips: [Trip]
}
}
The TS compiler gives this error: feed.ts(12,13): error TS2304: Cannot find name 'Trip'. (referring to trips: [Trip]). It doesn't say that the import failed or anything. I can even use trip inside the same file to create new objects var a = new Trip({}); without problem. Inside the interface it breaks.
Trip isn't a type, it's a variable, so you can do this:
let t = Trip;
let t2 = new Trip({});
But you can't do this:
let t: Trip;
You should change it to typeof Trip:
export interface IFeed extends mongoose.Document {
lastSnapshot: {
trips: [typeof Trip]
}
}
Also, if you want IFeed.lastSnapshot.trips to be an array, then it should be:
trips: typeof Trip[]
What you declared is a tuple of one item.
Edit
With an object the assignment is always the same (both js and ts):
let o = {
key: "value"
}
But when declaring types in typescript then you're not dealing with values:
interface A {
key: string;
}
let o: A = {
key: "value"
}
In the mongoose documentation they are using only javascript so all of their examples don't include the type declarations.