Conditional data depends on generic type - javascript

Basically I'm using a React function based component.
*** But the question has nothing to do with React specificly.
const Component = <Condition extends boolean>(props: React.PropsWithChildren<Props<Condition>>) => {
Props:
interface Props<Condition extends boolean> {
condition: Condition;
}
In this function, I create a variable to store some data.
const initialValues: Fields<Condition> = (() => {
const base = {
unit: '',
};
if (props.condition) {
return {
...base,
from2: '',
};
}
return base;
})();
The Fields type is configured as following:
interface Base {
unit: string;
}
interface Extended extends Base {
from2: string;
}
export type Fields<Condition extends boolean> = Condition extends true ? Extended : Base;
The entire code organized together:
interface Base {
unit: string;
}
interface Extended extends Base {
from2: string;
}
export type Fields<Condition extends boolean> = Condition extends true ? Extended : Base;
interface Props<Condition extends boolean> extends PropsFromState {
condition: Condition;
}
const Component = <Condition extends boolean>(props: React.PropsWithChildren<Props<Condition>>) => {
const initialValues: IJobFormFields<Condition> = (() => {
const base = {
unit: '',
};
if (props.condition) { // Check if condition (also Condition type) is true
return {
...base,
from2: '',
};
}
return base;
})();
};
The issue is that I receive the following error:
Type '{ unit: string; } | { unit: string; from2: string; }' is not assignable to type 'Fields<Condition>'.
Type '{ unit: string; }' is not assignable to type 'Fields<Condition>'.ts(2322)

That's a current design limitation of Typescript. It cannot narrow the type of conditional type depending on unspecified generic type parameter. And while the type parameter is not explicitly specified the type of Fields<Condition> is opaque to the compiler.
Usually similar cases when function returns a conditional type depending on a generic type parameter are good candidates for rewriting with function overloads. But since you're not returning the value of initialValues I believe you're better off splitting prop generation into separate branches:
const BaseComponent = (props: Base) => null
const ExtendedComponent = (props: Extended) => null
const Component = <T extends boolean>(props: Props<T>) => {
const base = { unit: '' }
if (props.condition) {
return <ExtendedComponent {...base} from2="" />
}
return <BaseComponent {...base} />
};
playground link

You can simply add the property with a question mark making it as an optional property.
unit?: string

Related

Passing props in React jsx as generics

In my react app i want to pass a specific interface as a generic into a unspecific component.
For example i have three specific interfaces
SpecificInterfaces.jsx
export interface InterfaceA {
name: string
age: number
...
}
export interface InterfaceB {
name: string
movies: string[]
count: number
...
}
export interface InterfaceC {
name: string
somestuff: someType
}
For each of the interfaces i have a specific component ComponentA, ComponentB and ComponentC.
These Components need to be used in a shared component ComponentShared.
Now for example i want in my ComponentA to return SharedComponent with the generic Type of InterfaceA and props of Type InterfaceA like this:
ComponentA.jsx
export interface Props<T> {
importData: T[]
... some props...
}
const props: Props<InterfaceA> = {
importData: importData //This is from Interface Type InterfaceA
... someProps ...
}
return (
<React.Fragment>
<SharedComponent<InterfaceA> {...props} />
</React.Fragment>
)
And in my sharedComponent i want to access the specific passed generic type like this:
SharedComponent.jsx
const SharedComponent= <T,>({
importData,
...the passed Props
}: Props<T>): JSX.Element => {
importData.map((data: T) =>
data.name)
At importData.map((data:T) => data.name) it throws an error, saying T has no member of name. So i guess something isnt working with my generics i pass in here, because the InterfaceA im passing in as generic has the member "name" like any ohter InterfaceB and InterfaceC. What am i doing wrong?
TypeScript doesn't know anything about the generic inside your function unless you inform it. You need to extend your generic T from a type that has the properties that you use inside the function. Consider this example:
TS Playground
function logNamesBroken <T>(objects: T[]): void {
for (const obj of objects) {
console.log(obj.name);
/* ^^^^
Property 'name' does not exist on type 'T'.(2339) */
}
}
type BaseObject = {
name: string;
};
function logNames <T extends BaseObject>(objects: T[]): void {
for (const obj of objects) {
console.log(obj.name); // ok now
}
}
More, based on the code in your question:
TS Playground
import {default as React} from 'react';
interface BaseItem {
name: string;
}
interface InterfaceA extends BaseItem {
age: number;
}
interface Props<T extends BaseItem> {
importData: T[];
}
const SharedComponent = <T extends BaseItem>({
importData,
}: Props<T>): React.ReactElement => {
return (
<ul>
{
importData.map((data, index) => (
<li key={`${index}-${data.name}`}>{data.name}</li>
))
}
</ul>
);
};
const importData: InterfaceA[] = [{name: 'a', age: 1}, {name: 'b', age: 2}];
const props: Props<InterfaceA> = {
importData,
};
const AnotherComponent = (): React.ReactElement => (
<React.Fragment>
<SharedComponent {...props} />
</React.Fragment>
);

typescript: How to define type as any part of enum?

I'm trying to create a translation module using typescript.
I want to define the languages as an enum parameter to create-text function, as like:
export enum Language {
He = "he",
En = "en",
}
const { createI18n, createI18nText } = createTextFunctions(Language);
const firstExample = createI18nText({
he: {
firstText: "שלום",
sc: {
hello: "שלום שוב"
}
},
en: {
firstText: "hello",
sc: {
hello: "hello again"
}
}
})
export const i18n = createI18n({
welcome: firstExample,
})
But my problem is that because the languages are are passed as params to a typescript function and the function infers the types, typescript is not alarming anything. I can create text with non-existing language and it will pass it,like createI18nText({ ar:{ hi : "hi" }}).
My text functions are these:
export type Languages = { [key: string]: string };
export const createTextFunctions = (languages: LanguagesD) => {
type I18nText<T extends object> = {
[k in keyof typeof languages]: T;
}
const createI18n = <T extends { [key: string]: I18nText<any>; }>(i18n: T) => {
return i18n;
};
const createI18nText = <T extends object>(text: I18nText<T>) => {
return text;
}
return {
createI18n,
createI18nText
}
}
So the code is running and doing whatever it needs to do, but I'm losing type control.
I do prefer to have my enum values lower-cased, so its an issue too. If this is the solution so I'll take it, but if there is any way to pass an enum-param and to run by its values it would be great.
You can make createTextFunctions use a generic. You will be able to customise the keys as you want when you create the text function :
// *L* must be a union of strings.
const createTextFunctions = <L extends string>() => {
type I18nText<T extends object> = Record<L, T>;
type I18n = Record<string, I18nText<any>>;
const createI18n = <T extends I18n>(i18n: T): T => {
return i18n;
};
const createI18nText = <T extends object>(text: I18nText<T>): I18nText<T> => {
return text;
}
return {
createI18n,
createI18nText
}
}
Then specify the Language as a union of strings:
type Language = "en" | "he";
And create/use the text functions:
const { createI18n, createI18nText } = createTextFunctions<Language>();
const firstExample = createI18nText({
he: {
firstText: "שלום",
sc: {
hello: "שלום שוב"
}
},
// If the key *en* is missing, typescript will complain.
en: {
firstText: "hello",
sc: {
hello: "hello again"
}
},
// If we add the key *us*, typescript will complain.
})
export const i18n = createI18n({
welcome: firstExample,
})
IMO union types are more comfortable to use than enums.

TypeScript DeepMap Union type with if statement

I'm trying to create a Union type with the structure of optional fields. I have created the following types:
export type StartEndType = {
start_date: string;
end_date: string;
};
export type PayrollContract = StartEndType & {
type: 'ON_PAYROLL';
yearly_holidays: number;
};
export type FreelanceContract = StartEndType & {
type: 'FREELANCE';
hourly_rate: number;
};
export type Contract = PayrollContract | FreelanceContract;
In my component it looks like:
{contractType === 'ON_PAYROLL' ? (
<Number name="yearly_holidays" />
) : contractType === 'FREELANCE' && (
<Number name="hourly_rate" />
)}
When I hover contract, it knows that it's one of ON_PAYROLL or FREELANCE. Although unfortunately I get a DeepMap error within my component.
Isn't thist supported by TypeScript out of the box?
Property 'yearly_holidays' does not exist on type 'DeepMap<PayrollContract, FieldError> | DeepMap<FreelanceContract, FieldError>'.
Property 'yearly_holidays' does not exist on type 'DeepMap<FreelanceContract, FieldError>'.
How can I solve this?
Thanks in advance.
Could you provide the code for Number component? Looks like an issue with how you are rendering the number by name, I was able to compile and use the TS code above fine in TS playground.
const renderPayField = (contract: Contract) => {
if (contract.type === 'ON_PAYROLL') {
return <Number name="yearly_holidays" />;
}
if (contract.type === 'FREELANCE') {
return <Number name="hourly_rate" />
}
return null;
};
Additionally, updated some of the TS to use interfaces + readonly to improve readability
export interface StartEndDate {
start_date: string;
end_date: string;
};
export interface PayrollContract extends StartEndDate {
readonly type: 'ON_PAYROLL';
yearly_holidays: number;
};
export interface FreelanceContract extends StartEndDate {
readonly type: 'FREELANCE';
hourly_rate: number;
};
export type Contract = PayrollContract | FreelanceContract;

React-Redux TypeScript Errors

Tryign to implement TypeScript with React/Redux and I'm running into some issues with my Types.
I have these types:
export interface IAuthState {
isSignedIn: Boolean | null;
userId: string | null;
}
export interface IStream {
id: string;
title: string;
description: string;
userId: string;
}
export interface IStreamState {
streams: { [index: string]: IStream };
}
Then I have two components:
interface IStreamsProps {
fetchStreams: () => void;
streams: IStreamState;
currentUserId: String | null;
isSignedIn: Boolean | null;
auth: IAuthState;
}
class StreamsList extends Component<IStreamsProps, IAppState> {
// Error: The Property Map does not exist on type IStreamState
renderList() {
return this.props.streams.map((stream: IStream) => (// CODE)
}
}
const mapStateToProps = (state: IAppState) => {
return {
streams: Object.values(state.streams),
currentUserId: state.auth.userId,
isSignedIn: state.auth.isSignedIn
};
};
export default connect(
mapStateToProps,
{ fetchStreams }
)(StreamsList);
Then I have another similar component:
const mapStateToProps = (state: IAppState, ownProps: HomeProps) => {
return {
//Error: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'IStreamState'
stream: state.streams[ownProps.match.params.id]
};
};
export default connect(
mapStateToProps,
null
)(StreamEdit);
How do I solve these two errors:
Error 1: The Property Map does not exist on type IStreamState
Error 2: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type 'IStreamState'
You incorrectly defined IStreamState. Look closely. You've defined it as an object
export interface IStreamState {
streams: { [index: string]: IStream }[];
}
// equivalent declaration using type:
export type IStreamState = {
streams: { [index: string]: IStream }[]
}
I'm sure you mean to type it as just an array, as so:
export type IStreamState = { [index: string]: IStream }[]
EDIT:
While not directly related to your question, you need to be careful with your types. I noticed you used IAppState in two different places. The class declarations are for props and local state. IAppState appears to be for your redux store.
// the second generic argument is for `this.state`
class StreamsList extends Component<IStreamsProps, IAppState> {
EDIT2:
Defining both arguments in a class is optional. If you leave the second one out, it defaults to {} for you.
It's impossible to be 100% sure as to why you're having that issue in your mapStateToProps, because I don't know what IAppState looks like. Just go double back and confirm your typing for state.streams is exactly what you expect.

Typescript React generic function type

I have a case:
interface State {
Mark: boolean;
Jane: boolean;
}
React.Component<{}, State> {
state = {
Mark: false,
Jane: false,
};
fn = (name: string) => () => {
this.setState({ [name]: true });
^^^^^^^^^^
}
render () {
return
['Mark', 'Jane'].map((name) => <div onClick={this.fn(name)}>{name}</div>);
}
Im getting error:
Argument of type '{ [x: string]: boolean; }' is not assignable
to parameter of type 'State | Pick<State, "Mark" | "Jane">
I could do two separate functions for it, but I want to keep it generic. How could I make it generic so the error goes off?
It turns out this is a limitation of the ts compiler itself. Check this issue for more on that: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/26635
Now to solve your problem you can use setState with a callback version. Like bellow:
interface State {
Mark: boolean;
Jane: boolean;
}
type StateKeys = keyof State
class Test extends React.Component <{}, State > {
state = {
Mark: false,
Jane: false,
};
fn = (name: StateKeys) => () => {
this.setState(prev => ({ ...prev, [name]: true }));
}
render() {
return ['Mark', 'Jane'].map((name: StateKeys) => <div onClick={this.fn(name)}>{name}</div>);
}
}

Categories