I don't understand why, in Typescript, I have this error in a varibale assignement; I think types are compatible, isn't it?
To work, I have to add:
async () => {
dataFiles = <Array<DataFileLoadingInstructionType>>await Promise.all(
but why?
The error:
Type
'(DataFile | { file_path: string; header_line: number; sheets: never[]; type: string; })[]'
is not assignable to type
'({ file_path: string; } & { file_info?: string | undefined; file_name_suffix?: string | undefined; command_parameters?: string[] | undefined; } & { type: "unknown"; })[]'.
This is my the index.ts example:
import { DataFileLoadingInstructionType } from "./types_async";
import * as path from "path";
type DataFile = {
file_path: string;
type: "unknown";
};
const originalDataFiles: Array<DataFile> = [];
originalDataFiles.push({ file_path: "myFile.txt", type: "unknown" });
let dataFiles: Array<DataFileLoadingInstructionType>;
async function convertPathIfLocal(dataFile: string) {
if (dataFile.indexOf("://") === -1 && !path.isAbsolute(dataFile)) {
dataFile = path.join("my_dir/", dataFile);
}
return dataFile;
}
(async () => {
//here, I have to add <Array<DataFileLoadingInstructionType>> to work
dataFiles = await Promise.all(
originalDataFiles
.filter((f) => f !== undefined)
.map(async (dataFile) => {
if (typeof dataFile === "string") {
return {
file_path: await convertPathIfLocal(dataFile),
header_line: 0,
sheets: [],
type: "csv",
};
} else {
dataFile.file_path = await convertPathIfLocal(dataFile.file_path);
return dataFile;
}
})
);
console.log(
`OUTPUT: ${JSON.stringify(
dataFiles
)} - type of dataFiles: ${typeof dataFiles}`
);
})();
And this is my the types.ts example:
import {
Array,
Literal,
Number,
Partial as RTPartial,
Record,
Static,
String,
Union,
} from "runtypes";
const UnknownDataFileLoadingInstructionTypeOptions = Record({
type: Literal("unknown"),
});
export const DataFileLoadingInstructionType = Record({ file_path: String })
.And(
RTPartial({
file_info: String,
file_name_suffix: String,
command_parameters: Array(String),
})
)
.And(Union(UnknownDataFileLoadingInstructionTypeOptions));
export type DataFileLoadingInstructionType = Static<
typeof DataFileLoadingInstructionType
>;
From what I read of these code snippets, types are actually not assignable.
On one hand, dataFiles is declared with the type Array<DataFileLoadingInstructionType>, in other words:
declare const dataFiles: Array<
| { file_path: string, file_info?: string, file_name_suffix?: string, command_parameters?: string[] }
| { type: 'unknown' }
>
On the other hand, the returned value of originalDataFiles.filter(...).map(...) is either:
{
file_path: string // await convertPathIfLocal(dataFile)
header_line: number // 0
sheets: never[] // [] inferred as Array<never>
type: string // "csv"
}
(cf. the returned object from the if branch, inside the map)
Or:
DataFile
(cf. the returned object from the else branch, inside the map)
So, we end up with:
dataFiles type:
Array<
| { file_path: string, file_info?: string, file_name_suffix?: string, command_parameters?: string[]}
| { type: 'unknown' }
>
await Promise.all(originalDataFiles.filter(...).map(...)) type:
Array<
| { file_path: string, header_line: number, sheets: never[], type: string }
| DataFile
>
And they are both, indeed, not assignable:
The properties header_line, sheets and type are missing on the type DataFileLoadingInstructionType
The property file_path is present in DataFile but not in UnknownDataFileLoadingInstructionTypeOptions
I'd say you should:
Add the 3 properties to the DataFileLoadingInstructionType, otherwise adapt the returned object in theif branch of the map to make it compatible with DataFileLoadingInstructionType.
Remove the property file_path from the dataFile returned in the else branch of the map, or add the file_path property to the UnknownDataFileLoadingInstructionTypeOptions type.
The essence of the problem lies in misunderstanding string literals, the limitations of type inferencing and duck typing. That is quite a lot, but I will try to explain it bit by bit.
Duck Typing
"If it walks like a duck and it quacks like a duck, then it must be a duck."
One of the nice things in Typescript that you do not need to instantiate a class in order for it to adhere to an interface.
interface Bird {
featherCount: number
}
class Duck implements Bird {
featherCount: number;
constructor(featherInHundreds: number){
this.featherCount = featherInHundreds * 100;
}
}
function AddFeathers(d1: Bird, d2: Bird) {
return d1.featherCount + d2.featherCount;
}
// The objects just need to have the same structure as the
// expected object. There is no need to have
AddFeathers(new Duck(2), {featherCount: 200});
This creates a lot of flexibility in the language. Flexibility you are using in the map function. You either create an entirely new object where you adjust some things or you adjust the existing dataFile. In this case that might easily be solved by creating a constructor or method that returns a new class. If there are a lot of transformations this might lead to very large classes.
Type Inferencing
However this flexibility comes at a cost in certain cases. Typescript needs to be able to infer the type, but in this case that goes wrong. When you create the new object, the type property is perceived as a string not the template literal "unknown". This exposes two problems in your code.
The type property can only contain a single value "unknown" because it is typed as a string literal with just one value as opposed to a union of multiple literals. The type should at least have the type of "unknown" | "csv" for this value to work. However I expect that this is just a problem in this example since adding the <Array<DataFileLoadingInstructionType>> seems to solve the problem for you, while in this example it would break the example.
But even if you would adjust this or pass the only allowed value "unknown" here it would still complain. This is how Typescript infers types, since you just assign a value here, it presumes that it is the more generic string as opposed to the more narrow literal "csv".
Solution
The trick is helping Typescript type the object you are creating. My suggestion would be to assert the type of the property type, so that Typescript knows that the string assignment is actually a string literal.
Example
import {
Literal,
Number,
Partial as RTPartial,
Record,
Static,
String,
Union,
} from "runtypes";
import * as path from "path";
// Create separate type, so that we don't need to assert the type inline.
type FileTypes = "unknown" | "csv"
const UnknownDataFileLoadingInstructionTypeOptions = Record({
type: Literal<FileTypes>("unknown"),
});
export const DataFileLoadingInstructionType = Record({ file_path: String })
.And(
RTPartial({
file_info: String,
file_name_suffix: String,
// Threw an error, commented it out for now.
// command_parameters: Array(String),
})
)
.And(Union(UnknownDataFileLoadingInstructionTypeOptions));
export type DataFileLoadingInstructionType = Static<
typeof DataFileLoadingInstructionType
>;
type DataFile = {
file_path: string;
type: FileTypes;
};
const originalDataFiles: Array<DataFile> = [];
originalDataFiles.push({ file_path: "myFile.txt", type: "unknown" });
let dataFiles: Array<DataFileLoadingInstructionType>;
async function convertPathIfLocal(dataFile: string) {
if (dataFile.indexOf("://") === -1 && !path.isAbsolute(dataFile)) {
dataFile = path.join("my_dir/", dataFile);
}
return dataFile;
}
(async () => {
//here, I have to add <Array<DataFileLoadingInstructionType>> to work
dataFiles = await Promise.all(
originalDataFiles
.filter((f) => f !== undefined)
.map(async (dataFile) => {
if (typeof dataFile === "string") {
return {
file_path: await convertPathIfLocal(dataFile),
header_line: 0,
sheets: [],
type: "csv" as FileTypes,
};
} else {
dataFile.file_path = await convertPathIfLocal(dataFile.file_path);
return dataFile;
}
})
);
console.log(
`OUTPUT: ${JSON.stringify(
dataFiles
)} - type of dataFiles: ${typeof dataFiles}`
);
})();
Related
I have a object like below
let Obj = {
['0'] : {
mode: 'x'
},
getMode: () => 'x'
}
Getting error when I create type definition like below
type Obj = {
[id: string]: {
mode: string
};
getMode: () => string
}
Getting error -- Property 'getMode' of type '() => string' is not assignable to 'string' index type '{ mode: string}'. ts(2411)
You are using index signature, so you have to specify other type with that signature.
type Obj = {
[id: string]: { mode: string} | (() => string);
}
let Obj: Obj = {
['0']: {
mode: 'x'
},
getMode: () => 'x'
}
This may be counterintuitive at first, but what you have is two definitions for getMode which are mutually exclusive
Within the type Obj you have the following:
A generic placeholder key [id: string] which basically says "any key which is a string should have the following shape" (in this case { mode: string })
An explicit key getMode whose value is of the shape () => string
As the first definition should be true for any key that is a string, it therefore clashes with the second definition. In other words, getMode is a string key and therefore satisfies the constraint [id: string], meaning it must be of the shape { mode: string }.
This concept (and potential workarounds) is explored in much more depth in similar SO answers such as this one
When you want to use a general key with { mode: string } type, all properties that you define which have string key type (including getMode) should have { mode: string } value type. One way of detaching getMode from general keys is to split your Obj type in two parts:
type GetModeType = {
getMode: () => string;
};
type GeneralType<T extends string> = {
[P in T as T extends "getMode" ? never : P]: { mode: string };
};
And then create a generic type based on their intersection:
type Obj<T extends string> = GetModeType & GeneralType<T>;
Then you can create an object based on Obj type:
const obj: Obj<"0" | "pending"> = {
getMode: () => "hello",
["0"]: { mode: "zero" },
pending: { mode: "this is pending mode." },
};
Notice that Obj type has getMode property with () => string type and the other keys that you want should be passed through as a generic type(string literals union).
You can read more about generic types and mapped types in typescript documentation.
In Typescript [key: string]: type means that ALL of the object keys should be of that type. You can solve this with intersection
type Obj = {
[id: string]: {
mode: string
};
} & {
getMode: () => string
}
I'v faced a problem trying to define an interface for the following structure:
interface JSONRecord {
[propName: string]: any;
}
type ReturnType = (id: string|number, field: string, record: JSONRecord) => string
export const formatDictionary = ({
mode = "render", key = "originalValue",
defaultKey = "originalValue"
}):ReturnType => (id, field, record) => {
...
}
interface Lookup {
Dictionary: ({mode, key, defaultKey}:{mode: string, key: string, defaultKey: string}) => ReturnType,
...
}
export const functionLookup:Lookup = {
Dictionary: formatDictionary,
...
}
export const formatField = (params:JSONRecord):string|ReturnType => {
const type:string = params.type
if (type === undefined) { return identity }
const fn = functionLookup[type]
if (fn === undefined) { return identity }
return fn({ ...params })
}
I'm getting the following errors:
In line const fn = functionLookup[type] : Element implicitly has an 'any' type becasue expression of type string can't be used to index type 'Lookup'. No index signature with parameter of type 'string' was found on type 'Lookup'.
I'm not really sure why is this happening, i thought that the Dictionary i defined in Lookup is supposed to be interpreted as a string. When i change Dictionary to [x: string]: ({mode, key, defaultKey}:{mode: string, key: string, defaultKey: string}) => ReturnType the error disappears, but i want to be specific with the arguments that can be passed.
In line return fn({ ...params }) : Expected 3 arguments, but got 1
I can't really wrap my head around this, the function is clearly expecting only 1 object as an argument {mode, key, defaultKey} or is it expecting the ReturnType function there?
I would appreciate any help. Thanks a lot in advance :)
In you case (from sandbox):
const anExampleVariable = "Hello World"
console.log(anExampleVariable)
// To learn more about the language, click above in "Examples" or "What's New".
// Otherwise, get started by removing these comments and the world is your playground.
interface Lookup {
test: number
}
const functionLookup:Lookup = {
test: 5
}
const params = {
type: 'test'
};
const type = params.type
const a = functionLookup[type]
params variable is infered as {type: string}.
Here functionLookup[type] you want use type as index for functionLookup, but TS does not work that way. Because you can't just use general type string as index for Lookup type.
Lookup allows you to use only literal test as index.
So you can add as const prefix to your params vvariable.
const params = {
type: 'test'
} as const;
You can make Lookup indexed:
interface Lookup {
test: number,
[prop:string]:number
}
Or, you can explicitly define a Record type for params:
const params:Record<string, keyof Lookup> = {
type: 'test'
}
i have the following
export type SelectedFilters = {
organization: string | null;
status: string[];
priority: string | null;
};
when declaring the variable in the component, it's like this:
selectedFilters: SelectedFilters = {
organization: null,
status: [],
priority: null,
};
then, i need to create another object from this, when user clicks on something in the page, where the change is basically in the array
like if
selectedFilters.status = ['filter1', 'filter2', 'filter3'];
i need to transform it to
newObject.status = 'filter1,filter2,filter3'
so i tried to create this function:
createQuery(): Dictionary<string | (string | null)[]> {
const query: Dictionary<string | (string | null)[]> = {};
Object.keys(this.selectedFilters).forEach((filter) => {
if (typeof this.selectedFilters[filter] !== 'object') {
query[filter] = this.selectedFilters[filter];
} else {
query[filter] = this.selectedFilters[filter].join(',');
}
});
return query;
}
but this gives me the following error:
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'SelectedFilters'.
No index signature with a parameter of type 'string' was found on type 'SelectedFilters'.
i need help fixing this problem
I'm struggling to get over an error on TS.
I define value (below in the code) according 2 interfaces that I created (WalletInformationsEdit and UserInformationsEdit)
The problem that I encounter is at the line right after the DB query, value[field] is underlined saying :
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'WalletInformationsEdit | UserInformationsEdit'.
No index signature with a parameter of type 'string' was found on type 'WalletInformationsEdit | UserInformationsEdit'.
I have find 2 solutions to avoid this error message, but I am not ok because they make my code less protected:
1/ In TS config, if I take off "strict": true and "noImplicitAny": true -> working
2/ if I define value with "any" type, -> working
But both are not worth it, I suppose.
Do you have any recommandation to handle this case ?
Thanks in advance,
Paul
public async update(value: WalletInformationsEdit | UserInformationsEdit): Promise<any> {
try {
// saving id before elem treatment
const keepId = value.id
delete value.id
let filterFields = []
for (let elem of Object.keys(value)) {
filterFields.push(elem)
}
let i = 0;
let updateList = [];
for (const field of filterFields) {
updateList.push(`"${field}" = $${++i}`);
}
const preparedQuery = {
text: `UPDATE "${this.constructor.name.toLowerCase()}" SET
${updateList.join()}
WHERE id = $${++i}
RETURNING *
`,
values: [...filterFields.map((field) => value[field]), keepId],
};
const result = await db.query(preparedQuery);
return result.rows[0]
} catch (error) {
throw new Error(error.message)
}
}
WalletInformationsEdit and UserInformationsEdit interfaces
export interface UserInformationsEdit {
id?: number,
email?: string,
password?: string,
country?: string
}
export interface WalletInformationsEdit {
id?: number,
name?: string,
is_default?: boolean,
}
I finally find the answer, to declare the index signature in my case, i had to declare it this way
export interface WalletInformationsEdit {
[key: string]: number | string | boolean | undefined;
id?: number,
name?: string,
is_default?: boolean
}
[key: string] -> index is read as a string
: number | string | boolean | undefined -> each type that composed my interface + undefined because properties are optional
thanks #DigitalDrifter for the link :-)
Context:
I'm trying to write a function that will allow the user of the function to define a certain type using no typescript type assertions (just plain old javascript syntax). Basically, I'm trying to write something like React's PropTypes but I want to map the type defined with these "PropTypes" to a type that can be enforced with typescript.
Maybe it's easier to understand what I'm trying to do by seeing the code. Below defineFunction is a function that returns a function that takes in an object defined by "PropTypes"`.
interface PropType<T> { isOptional: PropType<T | undefined> }
type V<Props> = {[K in keyof Props]: PropType<Props[K]>};
interface PropTypes {
string: PropType<string>,
number: PropType<number>,
bool: PropType<boolean>,
shape: <R>(definer: (types: PropTypes) => V<R>) => PropType<R>
}
// the only purpose of this function is to capture the type and map it appropriately
// it does do anything else
function defineFunction<Props, R>(props: (types: PropTypes) => V<Props>, func: (prop: Props) => R) {
return func;
}
// define a function
const myFunction = defineFunction(
// this is the interface definition that will be mapped
// to the actual required props of the function
types => ({
stringParam: types.string,
optionalNumberParam: types.number.isOptional,
objectParam: types.shape(types => ({
nestedParam: types.string,
}))
}),
// this is the function definition. the type of `props`
// is the result of the mapped type above
props => {
props.objectParam.nestedParam
return props.stringParam;
}
);
// use function
myFunction({
stringParam: '',
optionalNumberParam: 0,
objectParam: {
nestedParam: ''
}
});
Here is the resulting type of myFunction found by hovering over the type in VS Code:
const myFunction: (prop: {
stringParam: string;
optionalNumberParam: number | undefined;
objectParam: {
nestedParam: string;
};
}) => string
Question:
There is an issue with the code above--optionalNumberParam is correctly defined as number | undefined but it is not actually optional!
If I omit the optionalNumberParam, the typescript compiler will yell at me.
Is there anyway to assert that a type is optional instead of just T | undefined?
Replying to cale_b's comment:
Just a thought - have you tried optionalNumberParam?: types.number.isOptional
Yes and it's invalid syntax:
And to clarify, this defineFunction should let the user define a type using no typescript type assertions--instead everything should be inferred using mapped types. The ? is typescript only. I'm trying to write this function so that--theoretically--javascript users could define proptypes and still have the typescript compiler enforce those proptypes.
So, closest workaround I can do involves that two-object-literal solution I mentioned:
interface PropType<T> { type: T }
First I removed isOptional since it does you no good, and second I added a property with T in it since TypeScript can't necessarily tell the difference between PropType<T> and PropType<U> if T and U differ, unless they differ structurally. I don't think you need to use the type property though.
Then some stuff I didn't touch:
type V<Props> = {[K in keyof Props]: PropType<Props[K]>};
interface PropTypes {
string: PropType<string>,
number: PropType<number>,
bool: PropType<boolean>,
shape: <R>(definer: (types: PropTypes) => V<R>) => PropType<R>
}
function defineFunction<Props, R>(props: (types: PropTypes) => V<Props>, func: (prop: Props) => R) {
return func;
}
Now, I'm creating the function withPartial, which takes two parameters of types R and O and returns a value of type R & Partial<O>.
function withPartial<R, O>(required: R, optional: O): R & Partial<O> {
return Object.assign({}, required, optional);
}
Let's try it out:
const myFunction = defineFunction(
types => withPartial(
{
stringParam: types.string,
objectParam: types.shape(types => ({
nestedParam: types.string,
}))
},
{
optionalNumberParam: types.number
}
),
props => {
props.objectParam.nestedParam
return props.stringParam;
}
);
Note how I split the original object literal into two: one with required properties, and the other with optional ones, and recombine them using the withPartial function. Also note how the user of withPartial() doesn't need to use any TypeScript-specific notation, which is I think one of your requirements.
Inspecting the type of myFunction gives you:
const myFunction: (
prop: {
stringParam: string;
objectParam: {
nestedParam: string;
};
optionalNumberParam?: number;
}
) => string
which is what you want. Observe:
// use function
myFunction({
stringParam: '',
//optionalNumberParam: 0, // no error
objectParam: {
nestedParam: ''
}
});
Hope that helps; good luck!