mobily / ts-belt

🔧 Fast, modern, and practical utility library for FP in TypeScript.
https://mobily.github.io/ts-belt
MIT License
1.07k stars 30 forks source link

F.getWithDefault Coercion type problem #84

Open mattaiod opened 1 year ago

mattaiod commented 1 year ago

Hello everyone, first : thanks for this incredible library, look like the best in functionnal programmin in Ts

In the basic example

const res = pipe(
  [1, 2, 3, 4, 5], // → [1, 2, 3, 4, 5]
  A.dropExactly(2), // → Some([3, 4, 5])
  O.flatMap(A.head), // → Some(3)
  O.map(N.multiply(10)), // → Some(30)
) // → 30

O.getWithDefault(0)(res) // → 30

I have an typescript error at the last line:

Argument of type 'Option<number>' is not assignable to parameter of type 'Option<0>'.
  Type 'number' is not assignable to type 'Option<0>'.ts(2345)

The only way I have to fix that is to explicit precise the type like: O.getWithDefault(0 as number)(res) // → 30

The version not curried works: O.getWithDefault(res, 0)

I guess it's not normal so what is the reason and the solution?

Here my .eslintrc :

{ "extends": "@antfu", "plugins": [ "functional" ], "rules": { "@typescript-eslint/quotes": 0, "no-console": "off", "array-callback-return": "error", "no-constructor-return": "error", "no-duplicate-imports": "off", "no-new-native-nonconstructor": "error", "no-self-compare": "error", "no-template-curly-in-string": "error", "no-unused-private-class-members": "error", "class-methods-use-this": "error", "consistent-return": "error", "default-case": "error", "dot-notation": "error", "eqeqeq": "error", "init-declarations": "error", "no-eq-null": "error", "no-extend-native": "error", "no-implicit-coercion": "error", "@typescript-eslint/no-unused-vars": "off", "no-implicit-globals": "error", "no-new": "error", "no-new-func": "error", "no-new-object": "error", "no-var": "error", "prefer-object-spread": "error", "require-await": "error", "yoda": "error", "explicit-function-return-type": "off" } }

tsconfig.json:

{ "compilerOptions": { "noUnusedLocals": true, "noUnusedParameters": true, "baseUrl": ".", "module": "ESNext", "target": "ESNext", "lib": [ "DOM", "ESNext", "WebWorker" ], "strict": true, "esModuleInterop": true, "jsx": "preserve", "skipLibCheck": true, "moduleResolution": "node", "resolveJsonModule": true, "incremental": false, "noImplicitOverride": true, "noImplicitAny": true, "noImplicitThis": true, "strictFunctionTypes": true, "alwaysStrict": true, "strictBindCallApply": true, "strictPropertyInitialization": true, "useUnknownInCatchVariables": true, "allowUnusedLabels": false, "allowUnreachableCode": false, "noUncheckedIndexedAccess": true, "noPropertyAccessFromIndexSignature": true, "noFallthroughCasesInSwitch": true, "exactOptionalPropertyTypes": true, "noImplicitReturns": true, "strictNullChecks": true, "allowJs": false, "forceConsistentCasingInFileNames": true, "types": [ "vitest", "vite/client", "vue/ref-macros", "vite-plugin-pages/client", "vite-plugin-vue-layouts/client", ], "paths": { "~/": [ "src/" ] } }, "exclude": [ "dist", "node_modules", "cypress", "auto-imports.d.ts" ] }

Thank you in advance for your attention

JUSTIVE commented 12 months ago

give type parameter to O.getWithDefault. like

O.getWithDefault\<number>(0).

mattaiod commented 12 months ago

@JUSTIVE Yes, I'm familiar with this solution, but I'd like to know if there's a way to improve this function to have this default behavior.

probablykabari commented 9 months ago

@mattaiod you're issue above is because O.getWithDefault expects 0 to be an Option<number> type but it is a literal integer, take a look at the signature for why the non-curried version works:

export declare function getWithDefault<A>(option: Option<A>, defaultValue: NonNullable<A>): A;
export declare function getWithDefault<A>(defaultValue: NonNullable<A>): (option: Option<A>) => A;

As you see, the function would explicitly type A as 0 in the curried version whereas it would resolve to just number in the other, because normally the curried value would be within the pipe already and that would define the type. Basically don't use the curried version outside of a pipe/flow sequence OR type it yourself.

Note, there are a lot of instances where you need to do this when using getWithDefault. For example, the following would be a type error because the default is never[]

pipe(O.fromNullable([1, 2, 3]), O.getWithDefault([]))
JUSTIVE commented 5 months ago

How about making the default type an extended type of the given T type when it's curried? I just made a proof-of-concept version of this. image https://www.typescriptlang.org/play?strictNullChecks=false&jsx=0&ts=5.4.0-dev.20240121#code/JYWwDg9gTgLgBAbzMMBTANAeQL5wGZQQhwBEAAiBAEbAA2AngPQwDOAtFarTCQFAz00cTGBjAIAOwA8AFQB8cALxwZcAD5wJAV1q11cLRIAmqPMAmojvXo0YBjLVCioJ8UGFqoQLmAEMxkryoAB6QsHB2kizwAOaoMADqwDAAFgAipr468MqycgAUJnhZ3ABqvrRaqABcAHKStTq0vlSeeQCUigXB1SIB0vKdcrxwo3DBAPw9RSUw5ZWo1rZghJAslnDunt6u-uISQaHQ8JES0XBxicnpmdkATEpwsugAGnAhMC5GLHD1Eo26FpteSKP4A5qtVB5AozbLzKrVF5DfI9Pr7DpdEZjSbTW5lCpVawCIQASU+xGUJF8JH0JCoNLUJDsfBs9kczlcm3A2x8e0CyDQ+SxmAAdABlIiofJUkjtdCs0YAPQmWMuSVSGWK2WlzPaWNsyt4etZKwgaw2Wy8vP6vAFUuF4slUjJXgKMrlCrghtGauumtmdx1stZhvaQA

JUSTIVE commented 5 months ago

Maybe the NoInfer utility type from the Typescript 5.4.0 would be solution for this. updated example above, seems just working as intended